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",
|
"name": "Purchases",
|
||||||
"item": [
|
"item": [
|
||||||
|
|
@ -1185,11 +1298,6 @@
|
||||||
"key": "Accept",
|
"key": "Accept",
|
||||||
"value": "application/json",
|
"value": "application/json",
|
||||||
"type": "text"
|
"type": "text"
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "Authorization",
|
|
||||||
"value": "Bearer {{sanctum_token}}",
|
|
||||||
"type": "text"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"url": {
|
"url": {
|
||||||
|
|
@ -1217,11 +1325,6 @@
|
||||||
"value": "application/json",
|
"value": "application/json",
|
||||||
"type": "text"
|
"type": "text"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"key": "Authorization",
|
|
||||||
"value": "Bearer {{sanctum_token}}",
|
|
||||||
"type": "text"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"key": "Content-Type",
|
"key": "Content-Type",
|
||||||
"value": "application/json",
|
"value": "application/json",
|
||||||
|
|
@ -1256,11 +1359,6 @@
|
||||||
"key": "Accept",
|
"key": "Accept",
|
||||||
"value": "application/json",
|
"value": "application/json",
|
||||||
"type": "text"
|
"type": "text"
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "Authorization",
|
|
||||||
"value": "Bearer {{sanctum_token}}",
|
|
||||||
"type": "text"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"url": {
|
"url": {
|
||||||
|
|
@ -1415,11 +1513,6 @@
|
||||||
"key": "Accept",
|
"key": "Accept",
|
||||||
"value": "application/json",
|
"value": "application/json",
|
||||||
"type": "text"
|
"type": "text"
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "Authorization",
|
|
||||||
"value": "Bearer {{sanctum_token}}",
|
|
||||||
"type": "text"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"url": {
|
"url": {
|
||||||
|
|
@ -1444,7 +1537,14 @@
|
||||||
"script": {
|
"script": {
|
||||||
"type": "text/javascript",
|
"type": "text/javascript",
|
||||||
"exec": [
|
"exec": [
|
||||||
""
|
"const authToken = pm.collectionVariables.get('auth_token');",
|
||||||
|
"",
|
||||||
|
"if (authToken) {",
|
||||||
|
" pm.request.headers.upsert({",
|
||||||
|
" key: 'Authorization',",
|
||||||
|
" value: `Bearer ${authToken}`,",
|
||||||
|
" });",
|
||||||
|
"}"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -1461,7 +1561,12 @@
|
||||||
"variable": [
|
"variable": [
|
||||||
{
|
{
|
||||||
"key": "base_url",
|
"key": "base_url",
|
||||||
"value": "http://localhost:8000",
|
"value": "http://127.0.0.1:8000",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "auth_token",
|
||||||
|
"value": "",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -1528,11 +1633,6 @@
|
||||||
"key": "compra_id",
|
"key": "compra_id",
|
||||||
"value": "1",
|
"value": "1",
|
||||||
"type": "string"
|
"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\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
use Illuminate\Notifications\Notifiable;
|
use Illuminate\Notifications\Notifiable;
|
||||||
|
use Laravel\Sanctum\HasApiTokens;
|
||||||
|
|
||||||
#[Fillable(['nombre_apellido', 'email', 'password'])]
|
#[Fillable(['nombre_apellido', 'email', 'password'])]
|
||||||
#[Hidden(['password', 'remember_token'])]
|
#[Hidden(['password', 'remember_token'])]
|
||||||
class User extends Authenticatable
|
class User extends Authenticatable
|
||||||
{
|
{
|
||||||
/** @use HasFactory<UserFactory> */
|
/** @use HasFactory<UserFactory> */
|
||||||
use HasFactory, Notifiable;
|
use HasApiTokens, HasFactory, Notifiable;
|
||||||
|
|
||||||
protected static function newFactory(): UserFactory
|
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
|
<?php
|
||||||
|
|
||||||
|
use App\Domains\Auth\Controllers\LoginController;
|
||||||
|
use App\Domains\Auth\Controllers\LogoutController;
|
||||||
use App\Domains\Auth\Controllers\RegisterController;
|
use App\Domains\Auth\Controllers\RegisterController;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
Route::post('register', RegisterController::class);
|
Route::post('register', RegisterController::class);
|
||||||
|
Route::post('login', LoginController::class);
|
||||||
|
Route::middleware('auth:sanctum')->post('logout', LogoutController::class);
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^8.3",
|
"php": "^8.3",
|
||||||
"laravel/framework": "^13.8",
|
"laravel/framework": "^13.8",
|
||||||
"laravel/sanctum": "^4.0",
|
"laravel/sanctum": "^4.3",
|
||||||
"laravel/tinker": "^3.0",
|
"laravel/tinker": "^3.0",
|
||||||
"league/flysystem-aws-s3-v3": "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",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "c1933c09dc4d90a389d19d7cffff4e43",
|
"content-hash": "08dac456d84023cd816d5c96086a3b3c",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "aws/aws-crt-php",
|
"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