Sometimes you want to store users' files and make them accessible only to the file's owner or the site's admin (or any other authorization logic).
In this tutorial, we will be creating two routes to store and retrieve files with an authorization layer. I will assume you have your authentication system in place.
Let's say we want every user in our system to be able to upload their resume (a pdf file) to their profile. And we only want the owner of this resume and admins to have access to it.
public
directory, otherwise, everyone (even non-authenticated users) will have access to those files.
So we will be using storage/app/resumes
to store all users' resumes in the form of storage/app/resumes/{id}/resume.pdf
so that every user has a unique directory to store their resume.
We first define a route to handle the file upload.
Route::post('/resume', [UserResumeController::class, 'store'])
->middleware('auth')
->name('users.resume.store');
Note: a user must be authenticated to be able to upload a resume, hence the auth
middleware.
Then, we create a store
method in UserResumeController
to handle the above route:
public function store(Request $request)
{
$request->validate([
'resume' => 'file|mimetypes:application/pdf|max:4000'
]);
$request->file('resume')->store('resumes/' . auth()->user()->id . '/resume.pdf');
return back()->with('message', 'Resume uploaded successfully');
}
At this point, users can upload their resumes. Now to the part of making them accessible only by the user who created them and admin users.
Create an endpoint to show resumes:
Route::get('/resumes/{user}', [UserResumeController::class, 'show'])
->middleware('auth')
->name('users.resume.show');
Create a show
method in UserResumeController
to authorize users and fetch the appropriate resume:
public function show(User $user)
{
if (auth()->user()->is_admin || $user->is(auth()->user())) {
return Storage::download('resumes/' . $user->id . '/resume.pdf');
}
abort(401);
}
Now only admins and owners of the resume can view that resume.
Of course, you can change the way you store files ( storing the path of the file in the database or creating another table if there are multiple files per user ). This tutorial showed only the basics of protecting user uploads.
As I was writing this post, I used tests to quickly validate my code instead of creating blade views and opening the browser to test if it works. This is a nice way to decouple backend logic from front-end and quickly add some functionality without leaving your editor or using a REST api client like postman. This is the contents of the BIG test that validates the above code:
public function setUp(): void
{
parent::setUp();
Artisan::call('migrate');
}
/** @test */
public function test_only_owners_of_a_resume_and_admin_users_can_access_the_uploaded_resume()
{
Storage::fake('local');
$admin = User::factory()->create(['is_admin' => 1]);
$firstUser = User::factory()->create();
$secondUser = User::factory()->create();
$this->actingAs($firstUser);
Storage::assertMissing('resumes/' . $firstUser->id . '/resume.pdf');
$this->post(route('users.resume.store'), [
'resume' => UploadedFile::fake()->create('hello.pdf', 2000, 'application/pdf')
])->assertRedirect();
Storage::assertExists('resumes/' . $firstUser->id . '/resume.pdf');
$this->get(route('users.resume.show', $firstUser->id))
->assertStatus(200)
->assertDownload('resume.pdf');
$this->be($admin);
$this->get(route('users.resume.show', $firstUser->id))
->assertStatus(200)
->assertDownload('resume.pdf');
$this->be($secondUser);
$this->get(route('users.resume.show', $firstUser->id))
->assertStatus(401);
}
Thanks for completing this tutorial. let me know if you want to learn more about testing laravel apps