Fase 2: Implementación de Login, Store de Autenticación, Ruteo y Layout Protegido

This commit is contained in:
2025-12-17 13:13:43 -03:00
parent 1c7e8d5cdb
commit f19cd781ad
7 changed files with 185 additions and 30 deletions

View File

@@ -1,35 +1,20 @@
import { useState } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import './App.css'
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import Login from './pages/Login';
import Dashboard from './pages/Dashboard';
import ProtectedLayout from './layouts/ProtectedLayout';
function App() {
const [count, setCount] = useState(0)
return (
<>
<div>
<a href="https://vite.dev" target="_blank">
<img src={viteLogo} className="logo" alt="Vite logo" />
</a>
<a href="https://react.dev" target="_blank">
<img src={reactLogo} className="logo react" alt="React logo" />
</a>
</div>
<h1>Vite + React</h1>
<div className="card">
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
<p>
Edit <code>src/App.tsx</code> and save to test HMR
</p>
</div>
<p className="read-the-docs">
Click on the Vite and React logos to learn more
</p>
</>
)
<BrowserRouter>
<Routes>
<Route path="/login" element={<Login />} />
<Route element={<ProtectedLayout />}>
<Route path="/" element={<Dashboard />} />
</Route>
</Routes>
</BrowserRouter>
);
}
export default App
export default App;

View File

@@ -0,0 +1,43 @@
import { Navigate, Outlet } from 'react-router-dom';
import { useAuthStore } from '../store/authStore';
import { LogOut } from 'lucide-react';
export default function ProtectedLayout() {
const { isAuthenticated, logout } = useAuthStore();
if (!isAuthenticated) {
return <Navigate to="/login" replace />;
}
return (
<div className="min-h-screen bg-gray-100 flex">
{/* Sidebar minimalista por ahora */}
<aside className="w-64 bg-gray-900 text-white flex flex-col">
<div className="p-4 text-xl font-bold border-b border-gray-800">
SIG-CM
</div>
<nav className="flex-1 p-4">
<ul>
<li className="mb-2">
<a href="/" className="block p-2 hover:bg-gray-800 rounded">Dashboard</a>
</li>
</ul>
</nav>
<div className="p-4 border-t border-gray-800">
<button
onClick={logout}
className="flex items-center gap-2 text-gray-400 hover:text-white w-full"
>
<LogOut size={20} />
<span>Cerrar Sesión</span>
</button>
</div>
</aside>
{/* Main Content */}
<main className="flex-1 p-8">
<Outlet />
</main>
</div>
);
}

View File

@@ -0,0 +1,8 @@
export default function Dashboard() {
return (
<div>
<h1 className="text-3xl font-bold text-gray-800 mb-4">Bienvenido al Panel de Administración</h1>
<p className="text-gray-600">Seleccione una opción del menú para comenzar.</p>
</div>
);
}

View File

@@ -0,0 +1,75 @@
import { useState } from 'react';
import { useAuthStore } from '../store/authStore';
import { authService } from '../services/authService';
import { useNavigate } from 'react-router-dom';
import { LayoutDashboard, Lock } from 'lucide-react';
export default function Login() {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const setToken = useAuthStore((state) => state.setToken);
const navigate = useNavigate();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
const token = await authService.login(username, password);
setToken(token);
navigate('/');
} catch (err) {
setError('Credenciales inválidas');
}
};
return (
<div className="min-h-screen flex items-center justify-center bg-gray-900 text-white">
<div className="bg-gray-800 p-8 rounded-lg shadow-2xl w-96 border border-gray-700">
<div className="flex justify-center mb-6">
<div className="p-3 bg-blue-600 rounded-full">
<LayoutDashboard size={32} />
</div>
</div>
<h2 className="text-2xl font-bold text-center mb-6">SIG-CM Admin</h2>
{error && (
<div className="bg-red-500/20 border border-red-500 text-red-100 p-3 rounded mb-4 text-sm text-center">
{error}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-400 mb-1">Usuario</label>
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
className="w-full bg-gray-900 border border-gray-700 rounded p-2 focus:ring-2 focus:ring-blue-500 focus:outline-none"
placeholder="Ingrese su usuario"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-400 mb-1">Contraseña</label>
<div className="relative">
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full bg-gray-900 border border-gray-700 rounded p-2 focus:ring-2 focus:ring-blue-500 focus:outline-none pl-10"
placeholder="••••••••"
/>
<Lock className="absolute left-3 top-2.5 text-gray-500" size={16} />
</div>
</div>
<button
type="submit"
className="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded transition duration-200"
>
Ingresar
</button>
</form>
</div>
</div>
);
}

View File

@@ -0,0 +1,15 @@
import axios from 'axios';
const api = axios.create({
baseURL: 'https://localhost:7034/api', // Puerto HTTPS obtenido de launchSettings.json
});
api.interceptors.request.use((config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
export default api;

View File

@@ -0,0 +1,8 @@
import api from './api';
export const authService = {
login: async (username: string, password: string): Promise<string> => {
const response = await api.post('/auth/login', { username, password });
return response.data.token;
}
};

View File

@@ -0,0 +1,21 @@
import { create } from 'zustand';
interface AuthState {
token: string | null;
isAuthenticated: boolean;
setToken: (token: string) => void;
logout: () => void;
}
export const useAuthStore = create<AuthState>((set) => ({
token: localStorage.getItem('token'),
isAuthenticated: !!localStorage.getItem('token'),
setToken: (token: string) => {
localStorage.setItem('token', token);
set({ token, isAuthenticated: true });
},
logout: () => {
localStorage.removeItem('token');
set({ token: null, isAuthenticated: false });
},
}));