feat: implement authentication functionality with login and logout endpoints
This commit is contained in:
parent
a95cfc780b
commit
9a5a8e85b9
|
|
@ -1173,6 +1173,119 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Auth",
|
||||
"item": [
|
||||
{
|
||||
"name": "Register User",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Accept",
|
||||
"value": "application/json",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"nombre_apellido\": \"Ada Lovelace\",\n \"email\": \"ada@example.com\",\n \"password\": \"secret123\",\n \"password_confirmation\": \"secret123\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/register",
|
||||
"host": [
|
||||
"{{base_url}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"register"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Login User",
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"type": "text/javascript",
|
||||
"exec": [
|
||||
"const response = pm.response.json();",
|
||||
"",
|
||||
"if (response.token) {",
|
||||
" pm.collectionVariables.set('auth_token', response.token);",
|
||||
" console.log('auth_token saved to collection variables');",
|
||||
"} else {",
|
||||
" console.warn('Login response does not contain token');",
|
||||
"}"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Accept",
|
||||
"value": "application/json",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"email\": \"ada@example.com\",\n \"password\": \"secret123\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/login",
|
||||
"host": [
|
||||
"{{base_url}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"login"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Logout User",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Accept",
|
||||
"value": "application/json",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/logout",
|
||||
"host": [
|
||||
"{{base_url}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"logout"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Purchases",
|
||||
"item": [
|
||||
|
|
@ -1185,11 +1298,6 @@
|
|||
"key": "Accept",
|
||||
"value": "application/json",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{sanctum_token}}",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
|
|
@ -1217,11 +1325,6 @@
|
|||
"value": "application/json",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{sanctum_token}}",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json",
|
||||
|
|
@ -1256,11 +1359,6 @@
|
|||
"key": "Accept",
|
||||
"value": "application/json",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{sanctum_token}}",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
|
|
@ -1415,11 +1513,6 @@
|
|||
"key": "Accept",
|
||||
"value": "application/json",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{sanctum_token}}",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
|
|
@ -1444,7 +1537,14 @@
|
|||
"script": {
|
||||
"type": "text/javascript",
|
||||
"exec": [
|
||||
""
|
||||
"const authToken = pm.collectionVariables.get('auth_token');",
|
||||
"",
|
||||
"if (authToken) {",
|
||||
" pm.request.headers.upsert({",
|
||||
" key: 'Authorization',",
|
||||
" value: `Bearer ${authToken}`,",
|
||||
" });",
|
||||
"}"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
|
@ -1461,7 +1561,12 @@
|
|||
"variable": [
|
||||
{
|
||||
"key": "base_url",
|
||||
"value": "http://localhost:8000",
|
||||
"value": "http://127.0.0.1:8000",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"key": "auth_token",
|
||||
"value": "",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
|
|
@ -1528,11 +1633,6 @@
|
|||
"key": "compra_id",
|
||||
"value": "1",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"key": "sanctum_token",
|
||||
"value": "",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
namespace App\Domains\Auth\Controllers;
|
||||
|
||||
use App\Domains\Auth\Requests\LoginUserRequest;
|
||||
use App\Domains\Auth\Models\User;
|
||||
use App\Domains\Auth\Resources\UserResource;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class LoginController extends Controller
|
||||
{
|
||||
/**
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function __invoke(LoginUserRequest $request): JsonResponse
|
||||
{
|
||||
$credentials = $request->validated();
|
||||
$user = User::query()->where('email', $credentials['email'])->first();
|
||||
|
||||
if (! $user || ! Hash::check($credentials['password'], $user->password)) {
|
||||
throw ValidationException::withMessages([
|
||||
'email' => __('auth.failed'),
|
||||
]);
|
||||
}
|
||||
|
||||
$token = $user->createToken('api-token')->plainTextToken;
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Sesion iniciada correctamente.',
|
||||
'token' => $token,
|
||||
'token_type' => 'Bearer',
|
||||
'user' => UserResource::make($user),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
namespace App\Domains\Auth\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class LogoutController extends Controller
|
||||
{
|
||||
public function __invoke(Request $request): JsonResponse
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
$user->currentAccessToken()?->delete();
|
||||
$user->tokens()->delete();
|
||||
Auth::guard('web')->logout();
|
||||
|
||||
if ($request->hasSession()) {
|
||||
$request->session()->invalidate();
|
||||
$request->session()->regenerateToken();
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Sesion cerrada correctamente.',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -8,13 +8,14 @@ use Illuminate\Database\Eloquent\Attributes\Hidden;
|
|||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Laravel\Sanctum\HasApiTokens;
|
||||
|
||||
#[Fillable(['nombre_apellido', 'email', 'password'])]
|
||||
#[Hidden(['password', 'remember_token'])]
|
||||
class User extends Authenticatable
|
||||
{
|
||||
/** @use HasFactory<UserFactory> */
|
||||
use HasFactory, Notifiable;
|
||||
use HasApiTokens, HasFactory, Notifiable;
|
||||
|
||||
protected static function newFactory(): UserFactory
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
namespace App\Domains\Auth\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class LoginUserRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'email' => ['required', 'string', 'email', 'max:255'],
|
||||
'password' => ['required', 'string'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,10 @@
|
|||
<?php
|
||||
|
||||
use App\Domains\Auth\Controllers\LoginController;
|
||||
use App\Domains\Auth\Controllers\LogoutController;
|
||||
use App\Domains\Auth\Controllers\RegisterController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::post('register', RegisterController::class);
|
||||
Route::post('login', LoginController::class);
|
||||
Route::middleware('auth:sanctum')->post('logout', LogoutController::class);
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
"require": {
|
||||
"php": "^8.3",
|
||||
"laravel/framework": "^13.8",
|
||||
"laravel/sanctum": "^4.0",
|
||||
"laravel/sanctum": "^4.3",
|
||||
"laravel/tinker": "^3.0",
|
||||
"league/flysystem-aws-s3-v3": "3.0"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "c1933c09dc4d90a389d19d7cffff4e43",
|
||||
"content-hash": "08dac456d84023cd816d5c96086a3b3c",
|
||||
"packages": [
|
||||
{
|
||||
"name": "aws/aws-crt-php",
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ return [
|
|||
|
|
||||
*/
|
||||
|
||||
'guard' => ['web'],
|
||||
'guard' => [],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
<?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
|
||||
{
|
||||
if (Schema::hasTable('personal_access_tokens')) {
|
||||
return;
|
||||
}
|
||||
|
||||
Schema::create('personal_access_tokens', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->morphs('tokenable');
|
||||
$table->text('name');
|
||||
$table->string('token', 64)->unique();
|
||||
$table->text('abilities')->nullable();
|
||||
$table->timestamp('last_used_at')->nullable();
|
||||
$table->timestamp('expires_at')->nullable()->index();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('personal_access_tokens');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Feature\Auth;
|
||||
|
||||
use App\Domains\Auth\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Tests\TestCase;
|
||||
|
||||
class LoginControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_it_logs_in_a_user_and_returns_a_bearer_token(): void
|
||||
{
|
||||
$user = User::query()->create([
|
||||
'nombre_apellido' => 'Grace Hopper',
|
||||
'email' => 'grace@example.com',
|
||||
'password' => Hash::make('secret123'),
|
||||
]);
|
||||
|
||||
$response = $this->postJson('/api/login', [
|
||||
'email' => 'grace@example.com',
|
||||
'password' => 'secret123',
|
||||
]);
|
||||
|
||||
$response
|
||||
->assertOk()
|
||||
->assertJsonPath('message', 'Sesion iniciada correctamente.')
|
||||
->assertJsonPath('token_type', 'Bearer')
|
||||
->assertJsonPath('user.id', $user->id)
|
||||
->assertJsonPath('user.nombre_apellido', 'Grace Hopper')
|
||||
->assertJsonPath('user.email', 'grace@example.com');
|
||||
|
||||
$this->assertIsString($response->json('token'));
|
||||
$this->assertNotEmpty($response->json('token'));
|
||||
|
||||
$this->withHeader('Authorization', 'Bearer '.$response->json('token'))
|
||||
->getJson('/api/user')
|
||||
->assertOk()
|
||||
->assertJsonPath('id', $user->id)
|
||||
->assertJsonPath('email', 'grace@example.com');
|
||||
}
|
||||
|
||||
public function test_it_rejects_access_to_the_current_user_endpoint_without_a_token(): void
|
||||
{
|
||||
$this->getJson('/api/user')->assertUnauthorized();
|
||||
}
|
||||
|
||||
public function test_it_logs_out_the_current_token(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$token = $user->createToken('api-token')->plainTextToken;
|
||||
|
||||
$this->withHeader('Authorization', 'Bearer '.$token)
|
||||
->postJson('/api/logout')
|
||||
->assertOk()
|
||||
->assertJsonPath('message', 'Sesion cerrada correctamente.');
|
||||
|
||||
$this->assertDatabaseCount('personal_access_tokens', 0);
|
||||
}
|
||||
|
||||
public function test_it_returns_a_validation_error_for_invalid_credentials(): void
|
||||
{
|
||||
User::query()->create([
|
||||
'nombre_apellido' => 'Grace Hopper',
|
||||
'email' => 'grace@example.com',
|
||||
'password' => Hash::make('secret123'),
|
||||
]);
|
||||
|
||||
$this->postJson('/api/login', [
|
||||
'email' => 'grace@example.com',
|
||||
'password' => 'wrong-password',
|
||||
])->assertUnprocessable()->assertJsonValidationErrors(['email']);
|
||||
}
|
||||
|
||||
public function test_it_validates_required_login_fields(): void
|
||||
{
|
||||
$this->postJson('/api/login', [
|
||||
'email' => 'invalid-email',
|
||||
'password' => '',
|
||||
])->assertUnprocessable()->assertJsonValidationErrors([
|
||||
'email',
|
||||
'password',
|
||||
]);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue