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 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;
|
||||
|
||||
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