2212
Laravel Sanctum es una API paquete de autenticación para Laravel las aplicaciones, proporcionando un peso ligero, sencillo de usar un sistema de autenticación para una sola página de aplicaciones (SPAs), aplicaciones para móviles, y otras API. Ofrece token de autenticación basada en el uso de JSON Web de Tokens (JWT) o API tokens, la habilitación de la autenticación segura sin la sobrecarga de sesión tradicional basado en la autenticación. Sanctum simplifica la configuración de token de autenticación, permitiendo a los desarrolladores centrarse en la construcción de sus aplicaciones en lugar de lidiar con la autenticación de complejidades.
En este ejemplo se usará Sanctum, para autenticación y autorización con token bearer en las peticiones a endpoints.
El proyecto está disponible en éste repositorio, por si no requieren realizar el paso a paso. Sólo clonar, aplicar composer install y levantar el proyecto.
Crear el proyecto con el siguiente comando:
composer create-project laravel/laravel apidash
**El nombre del directorio del proyecto es apidash, lo pueden modificar en caso de ser necesario.
Instalar paquete API:
php artisan install:api
La base de datos se llamará ApiDashExample
Crear la base de datos en el gestor de base de datos que se haya elegido, ya sea phpMyAdmin o MySQL o cualquier otro.
Modificar el archivo .env del directorio raíz del proyecto, y ajustar la conexión a la base de datos, como lo son la contraseña, usuario IP, Puerto.
[...] DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=ApiDashExample DB_USERNAME=root DB_PASSWORD= [...]
BaseController : llevará el control de las respuestas y será el encargado de obtener las respuestas de los controladores como AuthController o PostController.
php artisan make:controller BaseController
El controlador debe tener éste código:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\JsonResponse;
use App\Http\Controllers\Controller;
class BaseController extends Controller
{
/**
* Enviar respuesta de éxito.
*
* @param mixed $data Datos de la respuesta
* @param string $message Mensaje a mostrar
* @param int $code Código de respuesta HTTP (por defecto 200)
* @return JsonResponse
*/
public function sendResponse($data, $message, $code = 200): JsonResponse
{
return response()->json([
'status' => 'success',
'code' => 200,
'message' => 'Conexión exitosa',
'resultado' => [
'status' => 'success',
'code' => $code,
'message' => $message,
'data' => $data,
],
], 200);
}
/**
* Enviar respuesta de error.
*
* @param string $message Mensaje de error
* @param array $errorMessages Errores adicionales (opcional)
* @param int $code Código de error (por defecto 400)
* @return JsonResponse
*/
public function sendError($message, $errorMessages = [], $code = 400): JsonResponse
{
return response()->json([
'status' => 'success',
'code' => 200,
'message' => 'Conexión exitosa',
'resultado' => [
'status' => 'danger',
'code' => $code,
'message' => is_array($errorMessages) ? implode(' ', $errorMessages) : $message,
],
], 200);
}
}
AuthController: Contiene las declaraciones de Login, Signup, y logout
php artisan make:controller AuthController
Código dentro del controlador:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Http\Controllers\BaseController as BaseController;
use Validator;
use Illuminate\Http\JsonResponse;
use App\Models\User;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
class AuthController extends BaseController
{
public function signup(Request $request): JsonResponse
{
$validator = Validator::make($request->all(), [
'name' => 'required|string',
'email' => 'required|string|email|unique:users,email',
'password' => 'required|string|min:4|confirmed',
]);
if ($validator->fails()) {
return $this->sendError('Error de validación.', $validator->errors()->all(), 401);
}
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password),
]);
$fullToken = $user->createToken('user-token')->plainTextToken;
$token = explode('|', $fullToken)[1];
return $this->sendResponse([], 'Usuario registrado correctamente.');
}
public function login(Request $request): JsonResponse
{
if (!Auth::attempt(['email' => $request->email, 'password' => $request->password])) {
return $this->sendError('Credenciales incorrectas.', ['El email o la contraseña son incorrectos.'], 401);
}
$user = Auth::user();
if ($user->status !== 1) {
return $this->sendError('Acceso denegado.', ['Su cuenta no está activa. Contacte al administrador.'], 403);
}
$fullToken = $user->createToken('MyApp')->plainTextToken;
$token = explode('|', $fullToken)[1];
$expiresAt = now()->addHour();
$user->tokens()->latest()->first()->update([
'expires_at' => $expiresAt,
]);
$data = [
'token' => $token,
'expires_at' => $expiresAt->toDateTimeString(),
'name' => $user->name,
'email' => $user->email,
'avatar' => $user->avatar,
];
return $this->sendResponse($data, 'Inicio de sesión exitoso.');
}
public function logout()
{
Auth::user()->tokens->each(function ($token) {
$token->forceDelete();
});
$response = [
'status' => 'success',
'code' => 200,
'message' => 'Conexión exitosa',
'resultado' => [
'status' => 'success',
'code' => 200,
'message' => 'Sesión finalizada.',
]
];
return response()->json( $response, 200);
}
}
PostController : Tendrá el código para administrar las opciones de creación de post de los usuarios
php artisan make:controller PostController
Código del controlador.
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\Post;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Auth;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
class PostController extends BaseController
{
public function get(Request $request): JsonResponse
{
try {
// Verificar si se busca un solo registro o una lista
if ($request->id > 0) {
$reg = Post::where('id', $request->id)
->where('user_id', Auth::id())
->first();
$regs = $reg ? [$reg] : [];
if ($reg) {
$total = 1;
}else{
$total = 0;
}
} else {
$query = Post::query();
if ($request->page > 0) {
$request->search = trim($request->search);
$query->where(function ($q) use ($request) {
$q->where('title', 'LIKE', '%' . $request->search . '%')
->orWhere('content', 'LIKE', '%' . $request->search . '%')
->orWhere('slug', 'LIKE', '%' . $request->search . '%');
});
$query = $query->where('user_id', Auth::id());
$total = $query->count();
$offset = ($request->page - 1) * $request->per_page;
$query = $query->offset($offset)->limit($request->per_page);
$query = $query->orderBy($request->order_by, $request->order);
$regs = $query->get();
} else {
$total = $query->where('user_id', Auth::id())->count();
$regs = $query->get();
}
}
return $this->sendResponse([
'total' => $total,
'regs' => $regs
], 'Listado de registros.');
} catch (UnauthorizedHttpException $e) {
return $this->sendError('Error de autenticación.', ['El token ha expirado o es inválido. Por favor, inicia sesión nuevamente.'], 401);
} catch (\Exception $e) {
return $this->sendError('Error en la operación.', [$e->getMessage()], 500);
}
}
public function delete(Request $request): JsonResponse
{
try {
$user = Auth::user();
$post = Post::where('id', $request->id)->where('user_id', $user->id)->first();
if (!$post) {
return $this->sendError('No encontrado.', ['El post no existe o no pertenece al usuario.'], 404);
}
if ($post->delete()) {
return $this->sendResponse([], 'Registro eliminado correctamente.');
} else {
return $this->sendError('Error al eliminar el registro.', [], 500);
}
} catch (UnauthorizedHttpException $e) {
return $this->sendError('Error de autenticación.', ['El token ha expirado o es inválido. Por favor, inicia sesión nuevamente.'], 401);
} catch (\Exception $e) {
return $this->sendError('Error en la operación.', [$e->getMessage()], 500);
}
}
public function save(Request $request): JsonResponse
{
try {
$user = Auth::user();
// Reglas de validación
$rules = [
'title' => 'required|string|max:300|unique:posts,title,' . $request->id,
'content' => 'nullable|string',
];
$messages = [
'title.required' => 'El título es obligatorio.',
'title.string' => 'El título debe ser un texto válido.',
'title.max' => 'El título no puede exceder los 300 caracteres.',
'title.unique' => 'El título ya ha sido registrado, elige otro.',
];
$validator = Validator::make($request->all(), $rules, $messages);
if ($validator->fails()) {
return $this->sendError('Error de validación.', [implode(' ', $validator->errors()->all())], 422);
}
$validated = $validator->validated();
$slug = Str::slug($validated['title'], '-');
if (Post::where('slug', $slug)->exists()) {
$slug .= '-' . Str::random(6);
}
if ($request->id > 0) {
$post = Post::where('id', $request->id)->where('user_id', $user->id)->first();
if (!$post) {
return $this->sendError('No encontrado.', ['El post no existe o no pertenece al usuario.'], 404);
}
$post->update([
'title' => $validated['title'],
'content' => $validated['content'],
'slug' => $slug,
]);
return $this->sendResponse([], 'La información del registro ha sido actualizada correctamente.');
} else {
$post = Post::create([
'title' => $validated['title'],
'content' => $validated['content'],
'slug' => $slug,
'user_id' => $user->id,
]);
return $this->sendResponse([], 'Registro grabado correctamente.');
}
} catch (\Exception $e) {
return $this->sendError('Error en la operación.', [$e->getMessage()], 500);
}
}
}
Creación de modelos y migraciones
User éste modelo ya existe previamente, modificarlo, y modificar la migración
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable
{
use HasFactory, Notifiable, HasApiTokens;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'name',
'email',
'password',
];
/**
* The attributes that should be hidden for serialization.
*
* @var array
*/
protected $hidden = [
'password',
'remember_token',
];
/**
* Get the attributes that should be cast.
*
* @return array
*/
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
];
}
}
Migración de User:
<?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
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->integer('status')->default(1);
$table->string('name');
$table->string('email')->unique();
$table->string('password');
$table->string('avatar')->nullable();
$table->timestamp('email_verified_at')->nullable();
$table->rememberToken();
$table->timestamps();
});
Schema::create('password_reset_tokens', function (Blueprint $table) {
$table->string('email')->primary();
$table->string('token');
$table->timestamp('created_at')->nullable();
});
Schema::create('sessions', function (Blueprint $table) {
$table->string('id')->primary();
$table->foreignId('user_id')->nullable()->index();
$table->string('ip_address', 45)->nullable();
$table->text('user_agent')->nullable();
$table->longText('payload');
$table->integer('last_activity')->index();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('users');
Schema::dropIfExists('password_reset_tokens');
Schema::dropIfExists('sessions');
}
};
Modelo post: administra los post o publicaciones que el usuario creará:
php artisan make:model Post -m
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
protected $primaryKey = 'id';
public $timestamps = false;
protected $fillable = ['title', 'content', 'slug', 'image', 'status', 'fc', 'user_id'];
public function author()
{
return $this->belongsTo(User::class, 'user_id', 'id');
}
}
Migración del modelo Post:
<?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
{
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->tinyInteger('status')->default(1)->comment('0 delete, 1 private, 2 public');
$table->string('title', 300)->nullable();
$table->text('content')->nullable();
$table->string('slug', 300)->nullable();
$table->string('image', 300)->nullable();
$table->timestamp('fc')->useCurrent();
$table->foreignId('user_id');
$table->foreign('user_id')->references('id')->on('users');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('posts');
}
};
Migrar la base de datos:
php artisan migrate:fresh --seed
php artisan serve
Desde el navegador se puede acceder y sólo se visualizará lo siguiente:
Y con eso se sabe que todo bien, ahora desde un cliente de API como postman lanzar las peticiones.
URL http://127.0.0.1:8000/api/v1/auth/signup Método: POST Body: { "name":"andy", "email":"andy@dev.com", "password_confirmation":"holamundo", "password":"holamundo" }
URL http://127.0.0.1:8000/api/v1/auth/login Método: POST Body: { "email":"andy@dev.com", "password":"holamundo" }
Del token que se recibió en login, se usará para poder enviar una petición en el save de post:
URL http://127.0.0.1:8000/api/v1/post/save Método: POST Body: { "id":0, "title": "Fedora dual boot con Windows 12", "content": "Contenido del POST" }
Se agrega en la pestaña Auth - Bearer Token
Listar todos los post del usuario logeado:
Explicación del body:
URL http://127.0.0.1:8000/api/v1/post/get Método: POST Body: { "id": 0, "page" : 1, "search": "", "order_by": "id", "order": "desc", "per_page": 10 }
Añadir en la pestaña Auth el Bearer Token:
Para eliminar un post, sólo se envía un ID del registro a eliminar.
URL http://127.0.0.1:8000/api/v1/post/delete Método: POST Body: { "id": 0, }
Y eso sería todo, cualquier comentario o mejora, no duden en dejarlos en la caja de comentarios.