feat: Sistema de autenticación frontend (Login + Register + Dashboard) #3

Merged
dmolinari merged 4 commits from feat/autenticacion-frontend into main 2026-04-01 17:38:49 +00:00
8 changed files with 37 additions and 20 deletions
Showing only changes of commit 4b44a8da08 - Show all commits

4
Frontend/.dockerignore Normal file
View File

@@ -0,0 +1,4 @@
node_modules
dist
.env
.env.local

1
Frontend/.env.example Normal file
View File

@@ -0,0 +1 @@
VITE_API_URL=http://localhost:5000/api

View File

@@ -1,16 +1,18 @@
import { Routes, Route, Navigate } from 'react-router-dom'; import { Routes, Route, Navigate } from 'react-router-dom';
import { AuthProvider } from './hooks/useAuth'; import { AuthProvider, useAuth } from './hooks/useAuth';
import { ProtectedRoute } from './components/ProtectedRoute'; import ProtectedRoute from './components/ProtectedRoute';
import LoginPage from './pages/LoginPage'; import LoginPage from './pages/LoginPage';
import RegisterPage from './pages/RegisterPage'; import RegisterPage from './pages/RegisterPage';
import DashboardPage from './pages/DashboardPage'; import DashboardPage from './pages/DashboardPage';
function App() { function App() {
const { isAuthenticated } = useAuth();
return ( return (
<AuthProvider> <AuthProvider>
<Routes> <Routes>
<Route path="/" element={ <Route path="/" element={
<Navigate to={localStorage.getItem('token') ? '/dashboard' : '/login'} replace /> <Navigate to={isAuthenticated ? '/dashboard' : '/login'} replace />
} /> } />
<Route path="/login" element={<LoginPage />} /> <Route path="/login" element={<LoginPage />} />
<Route path="/register" element={<RegisterPage />} /> <Route path="/register" element={<RegisterPage />} />

View File

@@ -1,5 +1,5 @@
import { LoginRequest, RegisterRequest, AuthResponse, RegisterResponse } from '../types/auth'; import type { LoginRequest, RegisterRequest, AuthResponse, RegisterResponse } from '../types/auth';
import { User } from '../types/user'; import type { User } from '../types/user';
const API_URL = import.meta.env.VITE_API_URL || ''; const API_URL = import.meta.env.VITE_API_URL || '';
@@ -24,10 +24,12 @@ class ApiClient {
throw new Error('Unauthorized'); throw new Error('Unauthorized');
} }
if (!response.ok) { if (!response.ok) {
const errorData = await response.json().catch(() => ({})); const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.message || `Error ${response.status}`); // Handle both { error: "message" } and { message: "..." } formats
} const errorMessage = errorData.error || errorData.message || `Error ${response.status}`;
throw new Error(errorMessage);
}
return response.json(); return response.json();
} }

View File

@@ -1,14 +1,15 @@
import { Navigate, Outlet } from 'react-router-dom'; import { Navigate } from 'react-router-dom';
import { useAuth } from '../hooks/useAuth'; import { useAuth } from '../hooks/useAuth';
import type { PropsWithChildren } from 'react';
const ProtectedRoute = () => { const ProtectedRoute = ({ children }: PropsWithChildren<{}>) => {
const { isAuthenticated } = useAuth(); const { isAuthenticated } = useAuth();
if (!isAuthenticated) { if (!isAuthenticated) {
return <Navigate to="/login" replace />; return <Navigate to="/login" replace />;
} }
return <Outlet />; return children;
}; };
export default ProtectedRoute; export default ProtectedRoute;

View File

@@ -1,7 +1,8 @@
import { createContext, useContext, useState, useEffect, ReactNode } from 'react'; import { createContext, useContext, useState, useEffect } from 'react';
import type { ReactNode } from 'react';
import { apiClient } from '../api/client'; import { apiClient } from '../api/client';
import { RegisterResponse, LoginRequest, RegisterRequest } from '../types/auth'; import type { LoginRequest, RegisterRequest } from '../types/auth';
import { User } from '../types/user'; import type { User } from '../types/user';
interface AuthContextProps { interface AuthContextProps {
user: User | null; user: User | null;

View File

@@ -18,8 +18,12 @@ const LoginPage = () => {
try { try {
await login({ username, password }); await login({ username, password });
navigate('/dashboard', { replace: true }); navigate('/dashboard', { replace: true });
} catch (err: any) { } catch (err) {
setError(err.message || 'Error al iniciar sesión'); if (err instanceof Error) {
setError(err.message || 'Error al iniciar sesión');
} else {
setError('Error al iniciar sesión');
}
} finally { } finally {
setLoading(false); setLoading(false);
} }

View File

@@ -20,12 +20,14 @@ const RegisterPage = () => {
try { try {
await register({ username, password, email, nombreCompleto }); await register({ username, password, email, nombreCompleto });
navigate('/dashboard', { replace: true }); navigate('/dashboard', { replace: true });
} catch (err: any) { } catch (err) {
// Handle 409 Conflict for duplicate username/email // Handle 409 Conflict for duplicate username/email
if (err.message?.includes('409')) { if (err instanceof Error && err.message?.includes('409')) {
setError('El usuario o correo electrónico ya existe'); setError('El usuario o correo electrónico ya existe');
} else { } else if (err instanceof Error) {
setError(err.message || 'Error al registrarse'); setError(err.message || 'Error al registrarse');
} else {
setError('Error al registrarse');
} }
} finally { } finally {
setLoading(false); setLoading(false);