Fase 2: Implementación de Login, Store de Autenticación, Ruteo y Layout Protegido
This commit is contained in:
@@ -1,35 +1,20 @@
|
|||||||
import { useState } from 'react'
|
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
||||||
import reactLogo from './assets/react.svg'
|
import Login from './pages/Login';
|
||||||
import viteLogo from '/vite.svg'
|
import Dashboard from './pages/Dashboard';
|
||||||
import './App.css'
|
import ProtectedLayout from './layouts/ProtectedLayout';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [count, setCount] = useState(0)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<BrowserRouter>
|
||||||
<div>
|
<Routes>
|
||||||
<a href="https://vite.dev" target="_blank">
|
<Route path="/login" element={<Login />} />
|
||||||
<img src={viteLogo} className="logo" alt="Vite logo" />
|
|
||||||
</a>
|
<Route element={<ProtectedLayout />}>
|
||||||
<a href="https://react.dev" target="_blank">
|
<Route path="/" element={<Dashboard />} />
|
||||||
<img src={reactLogo} className="logo react" alt="React logo" />
|
</Route>
|
||||||
</a>
|
</Routes>
|
||||||
</div>
|
</BrowserRouter>
|
||||||
<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>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App
|
export default App;
|
||||||
|
|||||||
43
frontend/admin-panel/src/layouts/ProtectedLayout.tsx
Normal file
43
frontend/admin-panel/src/layouts/ProtectedLayout.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
8
frontend/admin-panel/src/pages/Dashboard.tsx
Normal file
8
frontend/admin-panel/src/pages/Dashboard.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
75
frontend/admin-panel/src/pages/Login.tsx
Normal file
75
frontend/admin-panel/src/pages/Login.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
15
frontend/admin-panel/src/services/api.ts
Normal file
15
frontend/admin-panel/src/services/api.ts
Normal 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;
|
||||||
8
frontend/admin-panel/src/services/authService.ts
Normal file
8
frontend/admin-panel/src/services/authService.ts
Normal 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;
|
||||||
|
}
|
||||||
|
};
|
||||||
21
frontend/admin-panel/src/store/authStore.ts
Normal file
21
frontend/admin-panel/src/store/authStore.ts
Normal 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 });
|
||||||
|
},
|
||||||
|
}));
|
||||||
Reference in New Issue
Block a user