Skip to main content
POST
/
api
/
login
POST /api/login
curl --request POST \
  --url https://api.example.com/api/login \
  --header 'Content-Type: application/json' \
  --data '
{
  "user": "<string>",
  "password": "<string>"
}
'
{
  "success": true,
  "message": "<string>",
  "token": "<string>",
  "user": {
    "user.id": 123,
    "user.name": "<string>",
    "user.email": "<string>",
    "user.rol_id": 123,
    "user.id_empresa": 123
  },
  "empresas": [
    {
      "empresas[].id_empresa": 123,
      "empresas[].comercial": "<string>",
      "empresas[].ruc": "<string>",
      "empresas[].razon_social": "<string>",
      "empresas[].logo": "<string>",
      "empresas[].direccion": "<string>"
    }
  ],
  "permissions": [
    {}
  ]
}

Descripción

Endpoint público para autenticar usuarios con email o nombre de usuario. Retorna un token JWT válido por 8 horas junto con información del usuario, empresas disponibles y permisos asignados.

Características

  • Autenticación flexible: Acepta email o username en el mismo campo
  • Token de larga duración: 8 horas de validez (28,800 segundos)
  • Sesión dual: Genera token Sanctum + sesión PHP para PDFs
  • Carga de permisos: Retorna permisos del rol en el login
  • Multi-empresa: Admin recibe lista de todas las empresas

Request

Headers

Content-Type: application/json
Este endpoint no requiere autenticación previa (es público).

Body Parameters

user
string
required
Email o nombre de usuario. El sistema busca coincidencia en ambas columnas.Ejemplos:
password
string
required
Contraseña del usuario (mínimo 6 caracteres).

Response

success
boolean
required
Indica si la autenticación fue exitosa
message
string
required
Mensaje descriptivo del resultado
token
string
Token JWT para autenticación en requests subsecuentes. Incluir en header:
Authorization: Bearer {token}
user
object
Información básica del usuario autenticado
user.id
integer
ID único del usuario
user.name
string
Nombre de usuario (username)
user.email
string
Email del usuario
user.rol_id
integer
ID del rol asignado (1 = Administrador)
user.id_empresa
integer
ID de la empresa asignada
empresas
array
Lista de empresas disponibles para el usuario
  • Admin (rol_id = 1): Recibe todas las empresas activas
  • Usuario normal: Recibe solo su empresa asignada
empresas[].id_empresa
integer
ID único de la empresa
empresas[].comercial
string
Nombre comercial de la empresa
empresas[].ruc
string
RUC de la empresa (11 dígitos)
empresas[].razon_social
string
Razón social completa
URL del logo de la empresa
empresas[].direccion
string
Dirección fiscal de la empresa
permissions
array
Lista de permisos del usuario en formato resource.action
  • Admin (rol_id = 1): Recibe todos los permisos del sistema
  • Usuario normal: Recibe permisos asignados a su rol
Ejemplos:
  • ventas.view
  • ventas.create
  • productos.edit
  • caja.open

Ejemplos

curl -X POST "https://tu-dominio.com/api/login" \
  -H "Content-Type: application/json" \
  -d '{
    "user": "[email protected]",
    "password": "password123"
  }'

Respuestas

200 OK - Login Exitoso (Admin)

{
  "success": true,
  "message": "Login exitoso",
  "token": "1|abc123def456ghi789jklmnopqrstuvwxyz",
  "user": {
    "id": 1,
    "name": "admin",
    "email": "[email protected]",
    "rol_id": 1,
    "id_empresa": 1
  },
  "empresas": [
    {
      "id_empresa": 1,
      "comercial": "Santo Domingo",
      "ruc": "20612706702",
      "razon_social": "Santo Domingo S.A.C.",
      "logo": "https://tu-dominio.com/storage/logos/santo-domingo.png",
      "direccion": "Av. Principal 123, Lima, Perú"
    },
    {
      "id_empresa": 2,
      "comercial": "Sucursal Norte",
      "ruc": "20612706703",
      "razon_social": "Sucursal Norte S.A.C.",
      "logo": null,
      "direccion": "Jr. Comercio 456, Trujillo, Perú"
    }
  ],
  "permissions": [
    "ventas.view",
    "ventas.create",
    "ventas.edit",
    "ventas.delete",
    "productos.view",
    "productos.create",
    "productos.edit",
    "productos.delete",
    "clientes.view",
    "clientes.create",
    "clientes.edit",
    "clientes.delete",
    "caja.view",
    "caja.open",
    "caja.close",
    "caja.autorizar"
    // ... todos los permisos del sistema (Admin tiene todos)
  ]
}

200 OK - Login Exitoso (Usuario Normal)

{
  "success": true,
  "message": "Login exitoso",
  "token": "2|xyz789abc456def123ghijklmnopqrstuv",
  "user": {
    "id": 5,
    "name": "vendedor1",
    "email": "[email protected]",
    "rol_id": 2,
    "id_empresa": 1
  },
  "empresas": [
    {
      "id_empresa": 1,
      "comercial": "Santo Domingo",
      "ruc": "20612706702",
      "razon_social": "Santo Domingo S.A.C.",
      "logo": "https://tu-dominio.com/storage/logos/santo-domingo.png",
      "direccion": "Av. Principal 123, Lima, Perú"
    }
  ],
  "permissions": [
    "ventas.view",
    "ventas.create",
    "clientes.view",
    "productos.view"
  ]
}

401 Unauthorized - Usuario No Encontrado

{
  "success": false,
  "message": "Usuario no encontrado"
}

401 Unauthorized - Contraseña Incorrecta

{
  "success": false,
  "message": "Contraseña incorrecta"
}

422 Unprocessable Entity - Validación Fallida

{
  "success": false,
  "message": "Error de validación",
  "errors": {
    "user": [
      "El campo usuario es requerido"
    ],
    "password": [
      "El campo contraseña es requerido"
    ]
  }
}

Flujo de Autenticación

1

Cliente envía credenciales

POST a /api/login con user y password
2

Validación de entrada

$request->validate([
    'user' => 'required|string',
    'password' => 'required|string',
]);
3

Búsqueda flexible de usuario

$user = User::where('email', $request->user)
    ->orWhere('name', $request->user)
    ->first();
4

Verificación de contraseña

if (!Hash::check($request->password, $user->password)) {
    return response()->json([
        'success' => false,
        'message' => 'Contraseña incorrecta'
    ], 401);
}
5

Inicio de sesión dual

// Sesión PHP para rutas web
Auth::login($user);

// Token Sanctum para API
$token = $user->createToken('auth_token', ['*'], now()->addHours(8))
              ->plainTextToken;
6

Carga de empresas

  • Admin: Empresa::where('estado', '1')->get()
  • Usuario: Solo su empresa asignada
7

Carga de permisos

  • Admin: Permission::pluck('name')->toArray()
  • Usuario: $user->rol->permissions->pluck('name')->toArray()
8

Respuesta al cliente

Retorna JSON con token, usuario, empresas y permisos

Seguridad

Hash de Contraseñas

Las contraseñas se almacenan con bcrypt (cost factor 10):
$user->password = Hash::make($password);

Expiración de Tokens

Los tokens expiran después de 8 horas:
app/Http/Controllers/Api/AuthController.php:49
$token = $user->createToken('auth_token', ['*'], now()->addHours(8))
              ->plainTextToken;
Después de 8 horas, el cliente debe:
  1. Usar el endpoint /api/refresh para renovar el token, O
  2. Solicitar un nuevo login

Protección contra Fuerza Bruta

Recomendación: Implementa rate limiting en el servidor web:
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;

location /api/login {
    limit_req zone=login burst=10;
}
Esto permite máximo 5 intentos por minuto por IP.

Uso del Token

Después de obtener el token, inclúyelo en todas las peticiones:

Header Authorization

GET /api/ventas
Authorization: Bearer 1|abc123def456ghi789jklmnopqrstuvwxyz

Query Parameter (solo PDFs)

Para descargas de PDF que abren en nueva ventana:
https://tu-dominio.com/reporteNV/123?token=1|abc123def456ghi789jklmnopqrstuvwxyz
El middleware TokenFromQuery extrae el token del query param y lo coloca en el header automáticamente.

Códigos de Estado

CódigoDescripciónCausa
200OKLogin exitoso
401UnauthorizedUsuario no encontrado o contraseña incorrecta
422Unprocessable EntityValidación fallida (campos faltantes)
500Internal Server ErrorError de base de datos o servidor

Implementación en Frontend

React + Zustand

stores/authStore.js
import { create } from 'zustand';
import { persist } from 'zustand/middleware';

const useAuthStore = create(
  persist(
    (set) => ({
      user: null,
      token: null,
      permissions: [],
      empresas: [],
      
      login: async (user, password) => {
        const response = await fetch('/api/login', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ user, password })
        });
        
        const data = await response.json();
        
        if (data.success) {
          set({
            user: data.user,
            token: data.token,
            permissions: data.permissions,
            empresas: data.empresas
          });
          return true;
        }
        
        throw new Error(data.message);
      },
      
      logout: () => set({ user: null, token: null, permissions: [], empresas: [] }),
      
      hasPermission: (permission) => {
        const { permissions } = useAuthStore.getState();
        return permissions.includes(permission);
      }
    }),
    {
      name: 'auth-storage',
      getStorage: () => localStorage
    }
  )
);

export default useAuthStore;

Vue 3 + Pinia

stores/auth.js
import { defineStore } from 'pinia';

export const useAuthStore = defineStore('auth', {
  state: () => ({
    user: null,
    token: null,
    permissions: [],
    empresas: []
  }),
  
  actions: {
    async login(user, password) {
      const response = await fetch('/api/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ user, password })
      });
      
      const data = await response.json();
      
      if (data.success) {
        this.user = data.user;
        this.token = data.token;
        this.permissions = data.permissions;
        this.empresas = data.empresas;
        
        localStorage.setItem('auth_token', data.token);
        return true;
      }
      
      throw new Error(data.message);
    },
    
    logout() {
      this.user = null;
      this.token = null;
      this.permissions = [];
      this.empresas = [];
      localStorage.removeItem('auth_token');
    },
    
    hasPermission(permission) {
      return this.permissions.includes(permission);
    }
  },
  
  persist: true
});

Testing

PHPUnit

tests/Feature/AuthTest.php
class AuthTest extends TestCase
{
    public function test_login_with_valid_credentials()
    {
        $user = User::factory()->create([
            'email' => '[email protected]',
            'password' => Hash::make('password123')
        ]);

        $response = $this->postJson('/api/login', [
            'user' => '[email protected]',
            'password' => 'password123'
        ]);

        $response->assertStatus(200)
                 ->assertJson([
                     'success' => true,
                     'message' => 'Login exitoso'
                 ])
                 ->assertJsonStructure([
                     'token',
                     'user' => ['id', 'name', 'email', 'rol_id', 'id_empresa'],
                     'empresas',
                     'permissions'
                 ]);
    }
    
    public function test_login_with_invalid_password()
    {
        $user = User::factory()->create([
            'email' => '[email protected]',
            'password' => Hash::make('password123')
        ]);

        $response = $this->postJson('/api/login', [
            'user' => '[email protected]',
            'password' => 'wrongpassword'
        ]);

        $response->assertStatus(401)
                 ->assertJson([
                     'success' => false,
                     'message' => 'Contraseña incorrecta'
                 ]);
    }
}

Build docs developers (and LLMs) love