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.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *