DARK MODE 

Posted on Wednesday, June 1, 2022 by

Create Instagram Clone with Laravel (Part 11): Edit User's Profile and Protect The Edit Page

In this post, I covered from minute 2:31:48 until 2:54:50 of this video.

So we want to be able to edit the details on our profile. At this point, you should already know the steps when we want to create a new page. Link the page -> create the route -> create the method in controllers -> create the view.

Creating 'Edit Profile' page
First, let's add an 'edit profile' link in our profile. For now, let's just ignore about fancy styling. We can do that later. The link is would be /profile/{{ $user->id }}/edit (refer back to resource controllers). 
 
10
11
2
13
14
            <div class="d-flex justify-content-between align-items-baseline">
                <h2 class="font-weight-light">{{ $user->username }}</h2>
                <a href="/p/create">Add New Post</a>
            </div>
            <a href="/profile/{{ $user->id }}/edit">Edit Profile</a>
resources > views > profiles > index.blade.php

Create the route for the page.

26
27
Route::get('/profile/{user}', [App\Http\Controllers\ProfilesController::class, 'index'])->name('profile.show');
Route::get('/profile/{user}/edit', [App\Http\Controllers\ProfilesController::class, 'edit'])->name('profile.edit');
routes > web.php

Create the edit method in Profiles controller. We also can simplify the existing code in index method by using route model binding we've learned in previous part.

10
11
12
13
14
15
16
17
18
19
20
21
class ProfilesController extends Controller
{
    public function index(User $user)
    {
        return view('profiles/index', compact('user'));
    }

    public function edit(User $user)
    {
        return view('profiles/edit', compact('user'));
    }
}
app > Http > Controllers > ProfilesController.php

Notice that we use User instead of \App\Models\User? That's because we've already imported the class before with use App\Models\User; under the namespace.

Now, let's create a new view for edit profile in profile folder. Just copy the code from create.blade.php and do all the necessary changes to the name.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
@extends('layouts.app')

@section('content')
<div class="container">
    <form action="/profile/{{ $user->id }}" enctype="multipart/form-data" method="post">
        @csrf
        @method('PATCH')

        <div class="row">
            <div class="col-8 offset-2">
                <!-- Title -->
                <div class="row">
                    <h1>Edit Profile</h1>
                </div>

                <!-- Title -->
                <div class="form-group row">
                    <label for="title" class="col-md-4 col-form-label">Title</label>

                    <input id="title" type="text" class="form-control @error('title') is-invalid @enderror" name="title" value="{{ old('title') ?? $user->profile->title }}" autocomplete="title" autofocus>

                    @error('title')
                    <span class="invalid-feedback" role="alert">
                        <strong>{{ $message }}</strong>
                    </span>
                    @enderror
                </div>

                <!-- Description -->
                <div class="form-group row">
                    <label for="description" class="col-md-4 col-form-label">Description</label>

                    <input id="description" type="text" class="form-control @error('description') is-invalid @enderror" name="description" value="{{ old('description') ?? $user->profile->description }}" autocomplete="description" autofocus>

                    @error('description')
                    <span class="invalid-feedback" role="alert">
                        <strong>{{ $message }}</strong>
                    </span>
                    @enderror
                </div>

                <!-- URL -->
                <div class="form-group row">
                    <label for="url" class="col-md-4 col-form-label">URL</label>

                    <input id="url" type="text" class="form-control @error('url') is-invalid @enderror" name="url" value="{{ old('url') ?? $user->profile->url }}" autocomplete="url" autofocus>

                    @error('url')
                    <span class="invalid-feedback" role="alert">
                        <strong>{{ $message }}</strong>
                    </span>
                    @enderror
                </div>

                <!-- Image -->
                <div class="row">
                    <label for="image" class="col-md-4 col-form-label">Profile Image</label>

                    <input type="file" class="form-control-file" id="image" name="image">

                    @error('image')
                    <strong>{{ $message }}</strong>
                    @enderror
                </div>

                <!-- Add Button -->
                <div class="row pt-4">
                    <button class="btn btn-primary">Save</button>
                </div>
            </div>
        </div>

    </form>
</div>
@endsection
resources > views > profiles > edit.blade.php

Please take a look on the codes highlighted in blue.
  • /profile/{{ $user->id }} - refer back to resource controller (again) for the action of update
  • @method('PATCH') - blade directive for updating a portion or single attribute
  • {{ old('title') ?? $user->profile->title }} - this is to prefilled the fields with the current value so it will not just empty field. old('title') is for when you fail validation, and when you come back, those fields will be populated with the data that you entered. In our case, we add some extra code to pull the value from the table

Make updating work
We have completed in creating the edit profile page. But now, we can't use the form to update our profile just yet. Although we already use the PATCH method in the form, we still need to create the route and method for update.

26
27
28
Route::get('/profile/{user}', [App\Http\Controllers\ProfilesController::class, 'index'])->name('profile.show');
Route::get('/profile/{user}/edit', [App\Http\Controllers\ProfilesController::class, 'edit'])->name('profile.edit');
Route::patch('/profile/{user}', [App\Http\Controllers\ProfilesController::class, 'update'])->name('profile.update');
routes > web.php

And for the method:

19
20
21
22
23
24
25
26
27
28
29
30
31
32
    public function update(User $user)
    {

        $data = request()->validate([
            'title' => '',
            'description' => '',
            'url' => 'url',
            'image' => ''
        ]);

        auth()->user()->profile->update($data);

        return redirect("/profile/{$user->id}");
    }
app > Http > Controllers > ProfilesControler.php

There's a validation rule for url, so if you enter the field other than a url, it will return an error. However, you need to write the full url with the protocol. We'll fix that later.

At this point, if you try using the form and click Save, you will get this error:
Illuminate\Database\Eloqu ent\MassAssignmentException
Add [title] to fillable property to allow mass assignment on [App\Models\Profile].

To fix that, one last thing that you need to do is disabling mass assignment. This is because we are very particular about how we bring in each of those fields, as stated in line 23 - 26 of Profile controller. That protection that we get out of the box, we are disabling it as we're not just passing in the entire request into the update. Just add this one line of code in User  model.

 8
 9
10
11

17
class Profile extends Model
{
    protected $guarded = [];

    ...
}
app > Models > User.php


The form is now working. Try to update anything! But for now, let's leave the profile image empty first because we are gonna do that in the next part.





Protecting the 'Edit Profile' page / Restrict access for unauthorized user
Although the form is working, we have a problem. If we logged out and view some user's profile, we can also see the 'Edit Profile' link in the profile. For that, we are going to use 'policies'. Policies are a simple way for us to restrict what a user can or cannot do with a particular resource. Policies are associated to a specific model. In our case, we're going to do a policy for the profile.

Let's run php artisan help make:policy first to see what is the argument and option when creating a policy.


Run php artisan make:policy ProfilePolicy -m Profile, meaning that we are creating policy named ProfilePolicy for the model Profile.


So now, if we look in app folder, there's a new folder named Policies and inside of it there is ProfilePolicy.php. Each of the methods in the profile policy represents an action that can be taken on by Profile that based around the User. Each of these methods will going to return a boolean, either true or false. For example, if you return true in the view method, meaning that the User will be able to view the current Profile. Right now, we want to use the update method.

54
55
56
57
    public function update(User $user, Profile $profile)
    {
        return $user->id == $profile->user_id;
    }
app > Policies > ProfilePolicy.php

For the code above, we say that a user's id for the profile ($profile->user_id) must match the user's id ($user->id). It makes sense if you think about it. Let's say our profile has the user id of 1, viewing other user's profile with the id of 2. The other user's profile id doesn't match with our user id wasn't it? So, we supposedly shouldn't be able to view the /profile/2/edit. One more thing to do to make that work is we need to add an authorization in the edit and update method of the profile controller. 

15
16
17
18
19
20
    public function edit(User $user)
    {
        $this->authorize('update', $user->profile);

        return view('profiles/edit', compact('user'));
    }
21
22
23
24

35
    public function update(User $user)
    {
        $this->authorize('update', $user->profile);

        ...
    }
app > Http > Controllers > ProfilesController.php

The edit page is now protected. A user can no longer view the edit page of other users with a direct link.



Hide some elements and only show it to authorized user
Although all pages have been protected, of course we would want to hide the 'Add New Post' and 'Edit Profile' link on the profile. Right now, even if we not logged in, we could see the link in the profile. To solve this, it's actually very simple. We're going to wrap the link in a @can blade directive in the profile view.

10
11
12
13
14
15
16
17
18
            <div class="d-flex justify-content-between align-items-baseline">
                <h2 class="font-weight-light">{{ $user->username }}</h2>
                @can('update', $user->profile)
                <a href="/p/create">Add New Post</a>
                @endcan
            </div>
            @can('update', $user->profile)
            <a href="/profile/{{ $user->id }}/edit">Edit Profile</a>
            @endcan
resources > views > profiles > index.blade.php

 The final result:



We haven't got to editing the profile image yet. So in the next part, we're gonna do that.









No comments:

Post a Comment