18-Tutorial React - Consumir API Laravel con Autenticaci贸n JWT (Versi贸n Sencilla)
馃殌 Paso 1: Configuraci贸n Inicial (5 minutos)
# 1.1 Crear proyecto React
npx create-react-app gestor-tareas-jwt
cd gestor-tareas-jwt
# 1.2 Eliminar archivos innecesarios
rm src/App.test.js src/logo.svg src/reportWebVitals.js src/setupTests.js
# 1.3 Iniciar el proyecto
npm start馃搧 Paso 2: Estructura de Carpetas (2 minutos)
src/
├── components/
│ ├── LoginForm.js
│ ├── RegisterForm.js
│ ├── TareaList.js
│ ├── TareaForm.js
│ └── TareaItem.js
├── services/
│ └── authService.js
├── App.js
└── index.js馃搫 Paso 3: index.js (1 minuto)
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);馃摝 Paso 4: App.js inicial (2 minutos)
// src/App.js
import React, { useState } from 'react';
import LoginForm from './components/LoginForm';
import RegisterForm from './components/RegisterForm';
import TareaList from './components/TareaList';
import authService from './services/authService';
function App() {
const [usuario, setUsuario] = useState(authService.getUsuario());
const [mostrarRegistro, setMostrarRegistro] = useState(false);
const handleLogin = (user) => {
setUsuario(user);
};
const handleLogout = () => {
authService.logout();
setUsuario(null);
};
return (
<div style={{ padding: '20px' }}>
<h1>馃摑 Gestor de Tareas con JWT</h1>
{usuario ? (
<>
<div style={{ marginBottom: '20px' }}>
<span>Bienvenido, {usuario.name}</span>
<button
onClick={handleLogout}
style={{ marginLeft: '10px', padding: '5px 10px' }}
>
Cerrar Sesi贸n
</button>
</div>
<TareaList />
</>
) : (
<div style={{ maxWidth: '400px', margin: '0 auto' }}>
{mostrarRegistro ? (
<>
<RegisterForm
onSuccess={() => setMostrarRegistro(false)}
onCancel={() => setMostrarRegistro(false)}
/>
<p style={{ textAlign: 'center', marginTop: '10px' }}>
¿Ya tienes cuenta?{' '}
<button
onClick={() => setMostrarRegistro(false)}
style={{ background: 'none', border: 'none', color: 'blue', cursor: 'pointer' }}
>
Iniciar Sesi贸n
</button>
</p>
</>
) : (
<>
<LoginForm onSuccess={handleLogin} />
<p style={{ textAlign: 'center', marginTop: '10px' }}>
¿No tienes cuenta?{' '}
<button
onClick={() => setMostrarRegistro(true)}
style={{ background: 'none', border: 'none', color: 'blue', cursor: 'pointer' }}
>
Reg铆strate
</button>
</p>
</>
)}
</div>
)}
</div>
);
}
export default App;馃攽 Paso 5: Servicio de Autenticaci贸n (10 minutos)
// src/services/authService.js
const API_URL = 'http://localhost:8000/api';
class AuthService {
constructor() {
this.tokenKey = 'jwt_token';
this.userKey = 'user_data';
}
// Guardar token y usuario
setAuthData(token, user) {
localStorage.setItem(this.tokenKey, token);
localStorage.setItem(this.userKey, JSON.stringify(user));
}
// Obtener token
getToken() {
return localStorage.getItem(this.tokenKey);
}
// Obtener usuario
getUsuario() {
const user = localStorage.getItem(this.userKey);
return user ? JSON.parse(user) : null;
}
// Verificar si est谩 autenticado
isAuthenticated() {
return !!this.getToken();
}
// Limpiar datos de autenticaci贸n
logout() {
localStorage.removeItem(this.tokenKey);
localStorage.removeItem(this.userKey);
}
// Configuraci贸n para peticiones con token
getAuthHeaders() {
const token = this.getToken();
return {
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : '',
'Accept': 'application/json'
};
}
// 1. Iniciar sesi贸n
async login(email, password) {
try {
const response = await fetch(`${API_URL}/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({ email, password })
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || 'Error al iniciar sesi贸n');
}
const data = await response.json();
// Guardar token y datos del usuario
this.setAuthData(data.token, data.user);
return { success: true, user: data.user };
} catch (error) {
console.error('Error en login:', error);
return { success: false, message: error.message };
}
}
// 2. Registrarse
async register(name, email, password, password_confirmation) {
try {
const response = await fetch(`${API_URL}/register`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({
name,
email,
password,
password_confirmation
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || 'Error al registrarse');
}
const data = await response.json();
// Guardar token y datos del usuario
this.setAuthData(data.token, data.user);
return { success: true, user: data.user };
} catch (error) {
console.error('Error en registro:', error);
return { success: false, message: error.message };
}
}
// 3. Cerrar sesi贸n
async logoutApi() {
try {
const token = this.getToken();
if (token) {
await fetch(`${API_URL}/logout`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Accept': 'application/json'
}
});
}
} catch (error) {
console.error('Error en logout:', error);
} finally {
this.logout();
}
}
}
// Crear una instancia 煤nica
const authService = new AuthService();
export default authService;馃搵 Paso 6: Servicio de Tareas (10 minutos)
// src/services/tareaService.js
import authService from './authService';
const API_URL = 'http://localhost:8000/api';
class TareaService {
// Verificar autenticaci贸n antes de cada petici贸n
checkAuth() {
if (!authService.isAuthenticated()) {
throw new Error('No est谩s autenticado. Por favor, inicia sesi贸n.');
}
}
// Obtener headers con token
getHeaders() {
this.checkAuth();
return authService.getAuthHeaders();
}
// 1. Obtener todas las tareas del usuario
async obtenerTodas() {
try {
const response = await fetch(`${API_URL}/tareas`, {
method: 'GET',
headers: this.getHeaders()
});
if (response.status === 401) {
authService.logout();
throw new Error('Sesi贸n expirada. Por favor, inicia sesi贸n nuevamente.');
}
if (!response.ok) {
throw new Error('Error al obtener tareas');
}
const data = await response.json();
return data;
} catch (error) {
console.error('Error:', error);
throw error;
}
}
// 2. Crear nueva tarea
async crearTarea(tareaData) {
try {
const response = await fetch(`${API_URL}/tareas`, {
method: 'POST',
headers: this.getHeaders(),
body: JSON.stringify(tareaData)
});
if (response.status === 401) {
authService.logout();
throw new Error('Sesi贸n expirada');
}
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || 'Error al crear tarea');
}
return await response.json();
} catch (error) {
console.error('Error:', error);
throw error;
}
}
// 3. Actualizar tarea (solo para completar/descompletar)
async actualizarTarea(id, tareaData) {
try {
const response = await fetch(`${API_URL}/tareas/${id}`, {
method: 'PUT',
headers: this.getHeaders(),
body: JSON.stringify(tareaData)
});
if (response.status === 401) {
authService.logout();
throw new Error('Sesi贸n expirada');
}
if (!response.ok) {
throw new Error('Error al actualizar tarea');
}
return await response.json();
} catch (error) {
console.error('Error:', error);
throw error;
}
}
// 4. Eliminar tarea
async eliminarTarea(id) {
try {
const response = await fetch(`${API_URL}/tareas/${id}`, {
method: 'DELETE',
headers: this.getHeaders()
});
if (response.status === 401) {
authService.logout();
throw new Error('Sesi贸n expirada');
}
if (!response.ok) {
throw new Error('Error al eliminar tarea');
}
return await response.json();
} catch (error) {
console.error('Error:', error);
throw error;
}
}
}
// Crear una instancia 煤nica
const tareaService = new TareaService();
export default tareaService;✏️ Paso 7: LoginForm.js (10 minutos)
// src/components/LoginForm.js
import React, { useState } from 'react';
import authService from '../services/authService';
function LoginForm({ onSuccess }) {
const [formData, setFormData] = useState({
email: '',
password: ''
});
const [error, setError] = useState('');
const [cargando, setCargando] = useState(false);
const handleChange = (e) => {
const { name, value } = e.target;
setFormData({
...formData,
[name]: value
});
};
const handleSubmit = async (e) => {
e.preventDefault();
setError('');
setCargando(true);
try {
const resultado = await authService.login(formData.email, formData.password);
if (resultado.success) {
onSuccess(resultado.user);
} else {
setError(resultado.message || 'Error al iniciar sesi贸n');
}
} catch (error) {
setError('Error de conexi贸n. Verifica tu conexi贸n a internet.');
} finally {
setCargando(false);
}
};
return (
<div style={{ border: '1px solid #ccc', padding: '20px', borderRadius: '5px' }}>
<h2 style={{ marginTop: 0 }}>Iniciar Sesi贸n</h2>
{error && (
<div style={{ background: '#ffebee', color: '#c62828', padding: '10px', marginBottom: '15px', borderRadius: '4px' }}>
{error}
</div>
)}
<form onSubmit={handleSubmit}>
<div style={{ marginBottom: '15px' }}>
<label style={{ display: 'block', marginBottom: '5px' }}>Email:</label>
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
placeholder="tu@email.com"
style={{ width: '100%', padding: '8px' }}
required
disabled={cargando}
/>
</div>
<div style={{ marginBottom: '20px' }}>
<label style={{ display: 'block', marginBottom: '5px' }}>Contrase帽a:</label>
<input
type="password"
name="password"
value={formData.password}
onChange={handleChange}
placeholder="Tu contrase帽a"
style={{ width: '100%', padding: '8px' }}
required
disabled={cargando}
/>
</div>
<button
type="submit"
disabled={cargando}
style={{
width: '100%',
padding: '10px',
backgroundColor: '#4CAF50',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: cargando ? 'not-allowed' : 'pointer'
}}
>
{cargando ? 'Iniciando sesi贸n...' : 'Iniciar Sesi贸n'}
</button>
</form>
</div>
);
}
export default LoginForm;馃摑 Paso 8: RegisterForm.js (10 minutos)
// src/components/RegisterForm.js
import React, { useState } from 'react';
import authService from '../services/authService';
function RegisterForm({ onSuccess, onCancel }) {
const [formData, setFormData] = useState({
name: '',
email: '',
password: '',
password_confirmation: ''
});
const [error, setError] = useState('');
const [cargando, setCargando] = useState(false);
const handleChange = (e) => {
const { name, value } = e.target;
setFormData({
...formData,
[name]: value
});
};
const handleSubmit = async (e) => {
e.preventDefault();
// Validaci贸n b谩sica
if (formData.password !== formData.password_confirmation) {
setError('Las contrase帽as no coinciden');
return;
}
if (formData.password.length < 6) {
setError('La contrase帽a debe tener al menos 6 caracteres');
return;
}
setError('');
setCargando(true);
try {
const resultado = await authService.register(
formData.name,
formData.email,
formData.password,
formData.password_confirmation
);
if (resultado.success) {
onSuccess(resultado.user);
} else {
setError(resultado.message || 'Error al registrarse');
}
} catch (error) {
setError('Error de conexi贸n. Verifica tu conexi贸n a internet.');
} finally {
setCargando(false);
}
};
return (
<div style={{ border: '1px solid #ccc', padding: '20px', borderRadius: '5px' }}>
<h2 style={{ marginTop: 0 }}>Registrarse</h2>
{error && (
<div style={{ background: '#ffebee', color: '#c62828', padding: '10px', marginBottom: '15px', borderRadius: '4px' }}>
{error}
</div>
)}
<form onSubmit={handleSubmit}>
<div style={{ marginBottom: '15px' }}>
<label style={{ display: 'block', marginBottom: '5px' }}>Nombre:</label>
<input
type="text"
name="name"
value={formData.name}
onChange={handleChange}
placeholder="Tu nombre"
style={{ width: '100%', padding: '8px' }}
required
disabled={cargando}
/>
</div>
<div style={{ marginBottom: '15px' }}>
<label style={{ display: 'block', marginBottom: '5px' }}>Email:</label>
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
placeholder="tu@email.com"
style={{ width: '100%', padding: '8px' }}
required
disabled={cargando}
/>
</div>
<div style={{ marginBottom: '15px' }}>
<label style={{ display: 'block', marginBottom: '5px' }}>Contrase帽a:</label>
<input
type="password"
name="password"
value={formData.password}
onChange={handleChange}
placeholder="M铆nimo 6 caracteres"
style={{ width: '100%', padding: '8px' }}
required
disabled={cargando}
/>
</div>
<div style={{ marginBottom: '20px' }}>
<label style={{ display: 'block', marginBottom: '5px' }}>Confirmar Contrase帽a:</label>
<input
type="password"
name="password_confirmation"
value={formData.password_confirmation}
onChange={handleChange}
placeholder="Repite tu contrase帽a"
style={{ width: '100%', padding: '8px' }}
required
disabled={cargando}
/>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '10px' }}>
<button
type="submit"
disabled={cargando}
style={{
padding: '10px',
backgroundColor: '#2196F3',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: cargando ? 'not-allowed' : 'pointer'
}}
>
{cargando ? 'Registrando...' : 'Registrarse'}
</button>
<button
type="button"
onClick={onCancel}
disabled={cargando}
style={{
padding: '10px',
backgroundColor: '#9E9E9E',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: cargando ? 'not-allowed' : 'pointer'
}}
>
Cancelar
</button>
</div>
</form>
</div>
);
}
export default RegisterForm;馃搵 Paso 9: TareaList.js con CSS Grid (15 minutos)
// src/components/TareaList.js
import React, { useState, useEffect } from 'react';
import TareaForm from './TareaForm';
import TareaItem from './TareaItem';
import tareaService from '../services/tareaService';
function TareaList() {
const [tareas, setTareas] = useState([]);
const [cargando, setCargando] = useState(true);
const [error, setError] = useState('');
// CSS Grid para estructura b谩sica
const estiloContenedor = {
display: 'grid',
gridTemplateColumns: '300px 1fr', // 2 columnas
gap: '20px',
marginTop: '20px'
};
const estiloColumna = {
border: '1px solid #333',
padding: '15px'
};
// Cargar tareas al iniciar
useEffect(() => {
cargarTareas();
}, []);
// Funci贸n para cargar tareas
const cargarTareas = async () => {
try {
setCargando(true);
setError('');
const datos = await tareaService.obtenerTodas();
setTareas(datos);
} catch (error) {
if (error.message.includes('Sesi贸n expirada')) {
setError('Tu sesi贸n ha expirado. Por favor, inicia sesi贸n nuevamente.');
} else {
setError('Error al cargar tareas: ' + error.message);
}
} finally {
setCargando(false);
}
};
// Funci贸n para crear tarea
const manejarCrearTarea = async (nuevaTarea) => {
try {
await tareaService.crearTarea(nuevaTarea);
cargarTareas();
} catch (error) {
alert('Error al crear la tarea: ' + error.message);
}
};
// Funci贸n para eliminar tarea
const manejarEliminarTarea = async (id) => {
if (window.confirm('¿Eliminar esta tarea?')) {
try {
await tareaService.eliminarTarea(id);
cargarTareas();
} catch (error) {
alert('Error al eliminar la tarea: ' + error.message);
}
}
};
// Funci贸n para completar tarea
const manejarCompletarTarea = async (id, completada) => {
try {
await tareaService.actualizarTarea(id, { completada });
cargarTareas();
} catch (error) {
alert('Error al actualizar la tarea: ' + error.message);
}
};
return (
<div>
<div style={estiloContenedor}>
{/* COLUMNA IZQUIERDA: Formulario */}
<div style={estiloColumna}>
<h2 style={{ marginTop: 0 }}>➕ Nueva Tarea</h2>
<TareaForm onSubmit={manejarCrearTarea} />
</div>
{/* COLUMNA DERECHA: Lista de tareas */}
<div style={estiloColumna}>
<h2 style={{ marginTop: 0 }}>馃搵 Mis Tareas</h2>
{error && (
<div style={{ background: '#ffebee', color: '#c62828', padding: '10px', marginBottom: '15px', borderRadius: '4px' }}>
{error}
</div>
)}
{cargando ? (
<p>Cargando tus tareas...</p>
) : tareas.length === 0 ? (
<p>No tienes tareas. ¡Crea tu primera tarea!</p>
) : (
<>
<p>Total: {tareas.length} tareas</p>
{tareas.map((tarea) => (
<TareaItem
key={tarea.id}
tarea={tarea}
onEliminar={manejarEliminarTarea}
onCompletar={manejarCompletarTarea}
/>
))}
</>
)}
<div style={{ marginTop: '20px' }}>
<button
onClick={cargarTareas}
style={{
padding: '8px 15px',
backgroundColor: '#4CAF50',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>
馃攧 Recargar Tareas
</button>
</div>
</div>
</div>
</div>
);
}
export default TareaList;✏️ Paso 10: TareaForm.js (10 minutos)
// src/components/TareaForm.js
import React, { useState } from 'react';
function TareaForm({ onSubmit }) {
const [formData, setFormData] = useState({
nombre: '',
descripcion: '',
completada: false
});
const [error, setError] = useState('');
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
setFormData({
...formData,
[name]: type === 'checkbox' ? checked : value
});
};
const handleSubmit = (e) => {
e.preventDefault();
if (!formData.nombre.trim()) {
setError('El nombre de la tarea es obligatorio');
return;
}
onSubmit(formData);
setFormData({ nombre: '', descripcion: '', completada: false });
setError('');
};
return (
<form onSubmit={handleSubmit}>
{error && <p style={{ color: 'red' }}>{error}</p>}
<div style={{ marginBottom: '15px' }}>
<label style={{ display: 'block', marginBottom: '5px' }}>
Nombre de la tarea:
</label>
<input
type="text"
name="nombre"
value={formData.nombre}
onChange={handleChange}
placeholder="Ej: Comprar leche"
style={{ width: '100%', padding: '8px' }}
/>
</div>
<div style={{ marginBottom: '15px' }}>
<label style={{ display: 'block', marginBottom: '5px' }}>
Descripci贸n (opcional):
</label>
<textarea
name="descripcion"
value={formData.descripcion}
onChange={handleChange}
placeholder="Ej: Ir al supermercado"
rows="3"
style={{ width: '100%', padding: '8px' }}
/>
</div>
<div style={{ marginBottom: '20px' }}>
<label>
<input
type="checkbox"
name="completada"
checked={formData.completada}
onChange={handleChange}
/>
<span style={{ marginLeft: '5px' }}>Marcar como completada</span>
</label>
</div>
<button
type="submit"
style={{
width: '100%',
padding: '10px',
backgroundColor: '#4CAF50',
color: 'white',
border: 'none'
}}
>
➕ Crear Tarea
</button>
</form>
);
}
export default TareaForm;馃摝 Paso 11: TareaItem.js (10 minutos)
// src/components/TareaItem.js
import React from 'react';
function TareaItem({ tarea, onEliminar, onCompletar }) {
const estiloTarea = {
border: '1px solid #ccc',
padding: '15px',
marginBottom: '10px'
};
const estiloBotones = {
display: 'grid',
gridTemplateColumns: '1fr 1fr', // CSS Grid para 2 botones
gap: '10px',
marginTop: '10px'
};
return (
<div style={estiloTarea}>
<h3 style={{ marginTop: 0 }}>
{tarea.completada ? '✅ ' : '⏳ '}
{tarea.nombre}
</h3>
<p>{tarea.descripcion}</p>
<div style={{ color: '#666', fontSize: '14px', marginBottom: '10px' }}>
ID: {tarea.id} |
Estado: {tarea.completada ? 'Completada' : 'Pendiente'} |
Creada: {new Date(tarea.created_at).toLocaleDateString()}
</div>
<div style={estiloBotones}>
<button
onClick={() => onCompletar(tarea.id, !tarea.completada)}
style={{
padding: '8px',
backgroundColor: tarea.completada ? '#ff9800' : '#4CAF50',
color: 'white',
border: 'none'
}}
>
{tarea.completada ? '↩️ Desmarcar' : '✅ Completar'}
</button>
<button
onClick={() => onEliminar(tarea.id)}
style={{
padding: '8px',
backgroundColor: '#f44336',
color: 'white',
border: 'none'
}}
>
馃棏️ Eliminar
</button>
</div>
</div>
);
}
export default TareaItem;✅ Paso 12: Configuraci贸n de la API Laravel
Para que funcione, tu API Laravel necesita estas rutas:
// routes/api.php
use App\Http\Controllers\AuthController;
use App\Http\Controllers\TareaController;
// Autenticaci贸n
Route::post('/register', [AuthController::class, 'register']);
Route::post('/login', [AuthController::class, 'login']);
Route::post('/logout', [AuthController::class, 'logout'])->middleware('auth:sanctum');
// Tareas (protegidas)
Route::middleware('auth:sanctum')->group(function () {
Route::apiResource('tareas', TareaController::class);
});Y en Laravel instalar Sanctum:
composer require laravel/sanctum
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
php artisan migrate馃幆 Verificaci贸n Final
Estructura visual:
INICIO (No autenticado):
┌─────────────────────────┐
│ │
│ INICIAR SESI脫N │
│ Email: ___________ │
│ Contrase帽a: ______ │
│ [Iniciar Sesi贸n] │
│ │
│ ¿No tienes cuenta? │
│ [Reg铆strate] │
│ │
└─────────────────────────┘
AUTENTICADO:
┌──────────────────────────────────────┬──────────────────────────────────────┐
│ │ │
│ ➕ NUEVA TAREA │ 馃搵 MIS TAREAS │
│ [Nombre] _______________ │ Total: X tareas │
│ │ │
│ [Descripci贸n] _________ │ • ✅ Comprar leche │
│ ______ │ Ir al supermercado │
│ │ ID: 1 | Estado: Pendiente │
│ [ ] Marcar como completada │ [✅ Completar] [馃棏️ Eliminar] │
│ │ │
│ [➕ Crear Tarea] │ • ✅ Estudiar React │
│ │ Hacer ejercicios │
│ │ ID: 2 | Estado: Completada │
│ │ [↩️ Desmarcar] [馃棏️ Eliminar] │
│ │ │
│ │ [馃攧 Recargar Tareas] │
└──────────────────────────────────────┴──────────────────────────────────────┘馃搳 Resumen de Tiempo (90 minutos total)
| Paso | Tiempo | Descripci贸n |
|---|---|---|
| 1 | 5 min | Configuraci贸n inicial |
| 2 | 2 min | Estructura de carpetas |
| 3 | 1 min | index.js |
| 4 | 2 min | App.js inicial |
| 5 | 10 min | Servicio de autenticaci贸n |
| 6 | 10 min | Servicio de tareas |
| 7 | 10 min | LoginForm |
| 8 | 10 min | RegisterForm |
| 9 | 15 min | TareaList con CSS Grid |
| 10 | 10 min | TareaForm |
| 11 | 10 min | TareaItem |
| 12 | 5 min | Configuraci贸n Laravel |
✅ Caracter铆sticas implementadas:
1. Autenticaci贸n JWT completa:
✅ Registro de usuarios
✅ Inicio de sesi贸n
✅ Cierre de sesi贸n
✅ Persistencia de token en localStorage
✅ Headers autom谩ticos con token
2. CRUD de tareas:
✅ Listar tareas del usuario autenticado
✅ Crear nuevas tareas
✅ Marcar/desmarcar como completadas
✅ Eliminar tareas
3. Interfaz m铆nima:
✅ CSS Grid b谩sico para layout
✅ Formularios sencillos
✅ Mensajes de error claros
✅ Estados de carga
4. Seguridad:
✅ Token JWT en todas las peticiones
✅ Verificaci贸n autom谩tica de autenticaci贸n
✅ Manejo de sesiones expiradas
✅ Cada usuario solo ve sus tareas
馃挕 Tips para el estudiante:
Primero configurar Laravel con Sanctum para JWT
Probar las rutas API con Postman o cURL
Verificar localStorage para ver el token guardado
Usar Network tab en DevTools para ver headers
El c贸digo es m铆nimo pero funcional - perfecto para aprender
¡Perfecto! El alumno ahora tiene una aplicaci贸n React completa que consume una API Laravel con autenticaci贸n JWT, usando solo el CSS necesario para la estructura y sin funcionalidad de edici贸n.
Comentarios
Publicar un comentario