
Contexto
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.
Requerimientos
- PHP nativo 8.2 o superior o XAMPP con 8.2 o superior que contenga PHP 8.2 o superior
- MariaDB o MySQL
- Laravel 12
- Composer
- Terminal
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.
Creación de 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
Creación de la base datos
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= [...]
Crear controladores
BaseController
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
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
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);
}
}
}
Modelos
Creación de modelos y migraciones
Modelo User
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
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');
}
};
Migración de la Base de Datos
Migrar la base de datos:
php artisan migrate:fresh --seed

Levantar el servidor
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.
Pruebas
Registro:
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" }

Login:
URL http://127.0.0.1:8000/api/v1/auth/login Método: POST Body: { "email":"andy@dev.com", "password":"holamundo" }

Registro Post:
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 Posts
Listar todos los post del usuario logeado:
Explicación del body:
- id: si se envía 0, se listarán todas los registros que coincidan con la búsqueda, con el filtros, el ordenado, y la paginación, si es mayor a cero, se obtendrá un sólo registro con dicho ID, siempre y cuando exista.
- page: Es el número de la paginación y sólo se respeta si el di es 0
- search: lleva la búsqueda o filtro que se realiza del title y content
- order_by: se ordena de acuerdo al campo que lleve, puede ser cualquiera, y se usa en ordenamiento en tablas
- order: puede ser asc o desc
- per_page: es el total de registro a mostrar en una página
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:

Eliminar Post
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.