Laravel 10 API application with necessary functions pt 3.
Tags: laravel, php, vscode
Published: 2024-05-27 10:27:38

**Environment:** Windows 10

**Necessary prequisites (knowledge):** VS Code, Composer, Terminal, Git bash, GitHub, Postman, MarkDown, XAMPP (also serves my MySql database), environmental variables (or .env), Passport, TDD, Pest

If you require further details, please feel free to add in comments.

---

## User authentication, login, register
I am using Laravel Passport (https://laravel.com/docs/10.x/passport) for user authentication. 
`composer require laravel/passport`
then run the migrations 
`php artisan migrate` (in my case it didn't do anything)
Then install passport:
`php artisan passport:install`
This generated the necessary tables in the database, that's connected to Oauth. Explore these. I have 10 tables now.
In the User.php Model file, change the use Laravel\Sanctum\HasApiTokens; to use Laravel\Passport\HasApiTokens;
(I've noticed that my oauth_clients table was actually empty, so I had to re-run the install and re-create these. Probably because the default RefreshDatabase in the test deletes everything that's in the database, so I've changed this)

## Routing
This is where you tell the app, where the incoming API requests should be redirected to. 

## TDD to create register API endpoint
Run this to install Pest instead of phpunit
`composer remove phpunit/phpunit`
`composer require pestphp/pest --dev --with-all-dependencies`
initialise
`./vendor/bin/pest --init`
to run tests type:
`./vendor/bin/pest`
Then create the test file: tests/Feature/UserManagementTest.php

```
<?php

use Illuminate\Foundation\Testing\DatabaseTransactions;

uses(DatabaseTransactions::class);

it('allows a user to register', function () {
    $response = $this->postJson('/api/register', [
        'name' => 'John Doe',
        'email' => 'john@example.com',
        'password' => 'password',
        'password_confirmation' => 'password',
    ]);

    $response->assertStatus(200);
});
```
We create the route in the routes/api.php file (I've deleted the sanctum route that was there):

```
Route::group(['middleware' => ['cors', 'json.response']], function () {
    // public routes
    Route::post('/register', 'App\Http\Controllers\Auth\ApiAuthController@register')->name('register.api');
});

```
Run the test again: "Target class [cors] does not exist.". Run this to add Cors middleware:
`php artisan make:middleware Cors`
Replace the handler:

```
 return $next($request)
            ->header('Access-Control-Allow-Origin', '*')
            ->header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
            ->header('Access-Control-Allow-Headers', 'X-Requested-With, Content-Type, X-Token-Auth, Authorization');
```
`php artisan make:middleware ForceJsonResponse`
Replace the handler:

```
 $request->headers->set('Accept', 'application/json');
        return $next($request);
```
then add these to the kernel.php file,

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/eaeb9ecx0yl2frsknq4c.png)


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fbspnpo6ftujn6pw3esq.png)

Then run the test again: "Expected response status code [200] but received 500."

We don't have the controller yet, so create an Auth folder in Http/Controllers and add ApiAuthController.php file:

```
<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\Models\User;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
use Illuminate\Auth\Events\Registered;



class ApiAuthController extends Controller
{
    public function register(Request $request)
    {
        $validator = Validator::make($request->all(), [
            'name' => 'required|string|max:255',
            'email' => 'required|string|email|max:255|unique:users',
            'password' => 'required|string|min:6|confirmed',
        ]);
        if ($validator->fails()) {
            return response(['errors' => $validator->errors()->all()], 422);
        }
        $request['password'] = Hash::make($request['password']);
        $request['remember_token'] = Str::random(10);
        $user = User::create($request->toArray());

        try {
            $user->save();
            event(new Registered($user));

        } catch (Exception $e) {
            report($e);
            return false;
        }
    }

    public function login(Request $request)
    {
        $validator = Validator::make($request->all(), [
            'email' => 'required|string|email|max:255',
            'password' => 'required|string|min:6|confirmed',
        ]);
        if ($validator->fails()) {
            return response(['errors' => $validator->errors()->all()], 422);
        }
        $user = User::where('email', $request->email)->first();
        if ($user) {
            if (Hash::check($request->password, $user->password)) {
                $token = $user->createToken('Laravel Password Grant Client')->accessToken;
                return response([
                    'token' => $token,
                    'userId' => $user->id,
                    'successMessage' => "User successfully logged in"
                ], 200);
            } else {
                return response(['errors' => "Password mismatch"], 422);
            }
        } else {
            return response(['errors' => "User doesn't exist"], 422);
        }
    }

    public function logout(Request $request)
    {
        $token = $request->user()->token();
        $token->revoke();
        return response(['successMessage' => 'You have been successfully logged out!'], 200);
    }
    public function verifyEmail($id, $hash)
    {
        $user = User::find($id);
        if (!$user) {
            return response()->json(['message' => 'Invalid user'], 404);
        }
        if (!hash_equals($hash, sha1($user->getEmailForVerification()))) {
            return response()->json(['message' => 'Invalid verification link'], 401);
        }
        if ($user->hasVerifiedEmail()) {
            return redirect('/')->with('message', 'Email already verified');
        }
        $user->markEmailAsVerified();
        return redirect('/');
    }
}

```
Now the test should pass. If you run the request in postman, provide the body as in the test, you should be able to register. Make sure that php artisan serve is running.

## Login endpoint
Add this to UserManagementTest.php

```
it('allows a user to login', function () {
    // Create a user in the database
    $user = User::create([
        'name' => 'John Doe',
        'email' => 'john@example.com',
        'password' => Hash::make('password'),
        'password_confirmation' => Hash::make('password'),
    ]);

    // Attempt to log in with the created user's credentials
    $response = $this->postJson('/api/login', [
        'email' => 'john@example.com',
        'password' => 'password',
        'password_confirmation' => 'password',
    ]);

    // Assert the response status and structure
    $response->assertStatus(200)
             ->assertJsonStructure(['token']);
});

```
We need to add the router definition to fix the 404 error:
   

```
 Route::post('/login', 'App\Http\Controllers\Auth\ApiAuthController@login')->name('login.api');
```