From b93f4c5797105c674484ef8555ed5a9d2ca56911 Mon Sep 17 00:00:00 2001 From: Adrian Flores Cortes Date: Tue, 3 Feb 2026 04:25:10 -0600 Subject: [PATCH] [CONST-D-001] feat: Integrate API frontend with backend ## New Files - .env.example: Environment variables template - .env: Local development config (API URL: localhost:3021) - src/services/auth/auth.api.ts: Authentication API service - src/hooks/useAuth.ts: React Query hooks for auth - src/pages/auth/LoginPage.tsx: Functional login page ## Modified Files - src/App.tsx: Use LoginPage instead of placeholder - src/hooks/index.ts: Export useAuth hook ## Features - Login with email/password - JWT token management - Automatic token refresh - Error handling with toast notifications - Zustand + React Query integration Build: PASSED Co-Authored-By: Claude Opus 4.5 --- web/.env.example | 13 +++ web/src/App.tsx | 24 +---- web/src/hooks/index.ts | 1 + web/src/hooks/useAuth.ts | 171 ++++++++++++++++++++++++++++++ web/src/pages/auth/LoginPage.tsx | 158 +++++++++++++++++++++++++++ web/src/pages/auth/index.ts | 1 + web/src/services/auth/auth.api.ts | 117 ++++++++++++++++++++ web/src/services/auth/index.ts | 1 + 8 files changed, 465 insertions(+), 21 deletions(-) create mode 100644 web/.env.example create mode 100644 web/src/hooks/useAuth.ts create mode 100644 web/src/pages/auth/LoginPage.tsx create mode 100644 web/src/pages/auth/index.ts create mode 100644 web/src/services/auth/auth.api.ts create mode 100644 web/src/services/auth/index.ts diff --git a/web/.env.example b/web/.env.example new file mode 100644 index 0000000..f40d251 --- /dev/null +++ b/web/.env.example @@ -0,0 +1,13 @@ +# =========================================================== +# ERP CONSTRUCCION - CONFIGURACION FRONTEND +# =========================================================== +# Copiar este archivo a .env y ajustar valores + +# API Backend URL +VITE_API_URL=http://localhost:3021/api/v1 + +# Tenant ID para desarrollo +VITE_TENANT_ID=default-tenant + +# Ambiente +VITE_APP_ENV=development diff --git a/web/src/App.tsx b/web/src/App.tsx index e8f26b5..d30286c 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -18,6 +18,7 @@ import { ConceptosPage, PresupuestosPage, PresupuestoDetailPage, EstimacionesPag import { OpportunitiesPage, TendersPage, ProposalsPage, VendorsPage } from './pages/admin/bidding'; import { IncidentesPage, CapacitacionesPage, InspeccionesPage, InspeccionDetailPage } from './pages/admin/hse'; import { AvancesObraPage, BitacoraObraPage, ProgramaObraPage, ControlAvancePage } from './pages/admin/obras'; +import { LoginPage } from './pages/auth'; function App() { return ( @@ -88,8 +89,8 @@ function App() { {/* Portal Obra */} Obra Portal (TODO)} /> - {/* Auth routes placeholder */} - } /> + {/* Auth routes */} + } /> {/* 404 */} } /> @@ -99,23 +100,4 @@ function App() { ); } -function LoginPlaceholder() { - return ( -
-
-

Login

-

- Pagina de login placeholder. Por ahora accede directamente a /admin. -

- - Ir al Admin - -
-
- ); -} - export default App; diff --git a/web/src/hooks/index.ts b/web/src/hooks/index.ts index b92b432..e2bf92e 100644 --- a/web/src/hooks/index.ts +++ b/web/src/hooks/index.ts @@ -1,3 +1,4 @@ +export * from './useAuth'; export * from './useConstruccion'; export * from './usePresupuestos'; export * from './useReports'; diff --git a/web/src/hooks/useAuth.ts b/web/src/hooks/useAuth.ts new file mode 100644 index 0000000..4b3d6e7 --- /dev/null +++ b/web/src/hooks/useAuth.ts @@ -0,0 +1,171 @@ +/** + * useAuth Hook + * Hook para manejo de autenticación con React Query + */ + +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { AxiosError } from 'axios'; +import toast from 'react-hot-toast'; +import { useNavigate } from 'react-router-dom'; +import { useAuthStore } from '../stores/authStore'; +import { + login as loginApi, + register as registerApi, + logout as logoutApi, + getCurrentUser, + changePassword as changePasswordApi, + requestPasswordReset as requestPasswordResetApi, + LoginCredentials, + RegisterData, + AuthResponse, + User, +} from '../services/auth'; +import { ApiError } from '../services/api'; + +// Query Keys +const AUTH_KEYS = { + user: ['auth', 'user'] as const, +}; + +/** + * Hook para login + */ +export function useLogin() { + const queryClient = useQueryClient(); + const navigate = useNavigate(); + const { setUser, setTokens } = useAuthStore(); + + return useMutation, LoginCredentials>({ + mutationFn: loginApi, + onSuccess: (data) => { + setUser(data.user); + setTokens(data.accessToken, data.refreshToken); + queryClient.setQueryData(AUTH_KEYS.user, data.user); + toast.success('Bienvenido'); + navigate('/admin/dashboard'); + }, + onError: (error) => { + const message = error.response?.data?.message || 'Error al iniciar sesión'; + toast.error(message); + }, + }); +} + +/** + * Hook para registro + */ +export function useRegister() { + const queryClient = useQueryClient(); + const navigate = useNavigate(); + const { setUser, setTokens } = useAuthStore(); + + return useMutation, RegisterData>({ + mutationFn: registerApi, + onSuccess: (data) => { + setUser(data.user); + setTokens(data.accessToken, data.refreshToken); + queryClient.setQueryData(AUTH_KEYS.user, data.user); + toast.success('Cuenta creada exitosamente'); + navigate('/admin/dashboard'); + }, + onError: (error) => { + const message = error.response?.data?.message || 'Error al crear cuenta'; + toast.error(message); + }, + }); +} + +/** + * Hook para logout + */ +export function useLogout() { + const queryClient = useQueryClient(); + const navigate = useNavigate(); + const { logout: clearAuth } = useAuthStore(); + + return useMutation({ + mutationFn: logoutApi, + onSuccess: () => { + clearAuth(); + queryClient.clear(); + navigate('/auth/login'); + toast.success('Sesión cerrada'); + }, + onError: () => { + // Aún así limpiar el estado local + clearAuth(); + queryClient.clear(); + navigate('/auth/login'); + }, + }); +} + +/** + * Hook para obtener usuario actual + */ +export function useCurrentUser() { + const { isAuthenticated, user: storedUser } = useAuthStore(); + + return useQuery>({ + queryKey: AUTH_KEYS.user, + queryFn: getCurrentUser, + enabled: isAuthenticated, + initialData: storedUser || undefined, + staleTime: 5 * 60 * 1000, // 5 minutos + }); +} + +/** + * Hook para cambiar contraseña + */ +export function useChangePassword() { + return useMutation, { currentPassword: string; newPassword: string }>({ + mutationFn: ({ currentPassword, newPassword }) => + changePasswordApi(currentPassword, newPassword), + onSuccess: () => { + toast.success('Contraseña actualizada'); + }, + onError: (error) => { + const message = error.response?.data?.message || 'Error al cambiar contraseña'; + toast.error(message); + }, + }); +} + +/** + * Hook para solicitar reset de contraseña + */ +export function useRequestPasswordReset() { + return useMutation, string>({ + mutationFn: requestPasswordResetApi, + onSuccess: () => { + toast.success('Se envió un correo con instrucciones'); + }, + onError: (error) => { + const message = error.response?.data?.message || 'Error al solicitar reset'; + toast.error(message); + }, + }); +} + +/** + * Hook principal de autenticación + * Combina estado de Zustand con queries + */ +export function useAuth() { + const { isAuthenticated, user, accessToken, logout: clearAuth } = useAuthStore(); + const loginMutation = useLogin(); + const logoutMutation = useLogout(); + + return { + isAuthenticated, + user, + accessToken, + login: loginMutation.mutate, + loginAsync: loginMutation.mutateAsync, + logout: logoutMutation.mutate, + isLoggingIn: loginMutation.isPending, + isLoggingOut: logoutMutation.isPending, + clearAuth, + }; +} diff --git a/web/src/pages/auth/LoginPage.tsx b/web/src/pages/auth/LoginPage.tsx new file mode 100644 index 0000000..3624cae --- /dev/null +++ b/web/src/pages/auth/LoginPage.tsx @@ -0,0 +1,158 @@ +/** + * LoginPage + * Página de inicio de sesión para ERP Construcción + */ + +import { useState } from 'react'; +import { Link } from 'react-router-dom'; +import { useLogin } from '../../hooks/useAuth'; +import { Building2, Mail, Lock, Eye, EyeOff, Loader2 } from 'lucide-react'; + +export function LoginPage() { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [showPassword, setShowPassword] = useState(false); + const loginMutation = useLogin(); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (!email || !password) return; + loginMutation.mutate({ email, password }); + }; + + return ( +
+
+ {/* Logo y Título */} +
+
+ +
+

ERP Construcción

+

Ingresa a tu cuenta

+
+ + {/* Formulario */} +
+
+ {/* Email */} +
+ +
+
+ +
+ setEmail(e.target.value)} + className="block w-full pl-10 pr-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors" + placeholder="usuario@empresa.com" + /> +
+
+ + {/* Password */} +
+ +
+
+ +
+ setPassword(e.target.value)} + className="block w-full pl-10 pr-12 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors" + placeholder="••••••••" + /> + +
+
+ + {/* Opciones */} +
+ + + ¿Olvidaste tu contraseña? + +
+ + {/* Submit Button */} + +
+ + {/* Divider */} +
+
+
+
+
+ o +
+
+ + {/* Demo Access */} +
+

¿Quieres ver una demo?

+ + Acceder sin cuenta (modo demo) + +
+
+ + {/* Footer */} +

+ © 2026 ERP Construcción. Todos los derechos reservados. +

+
+
+ ); +} diff --git a/web/src/pages/auth/index.ts b/web/src/pages/auth/index.ts new file mode 100644 index 0000000..2c983e3 --- /dev/null +++ b/web/src/pages/auth/index.ts @@ -0,0 +1 @@ +export { LoginPage } from './LoginPage'; diff --git a/web/src/services/auth/auth.api.ts b/web/src/services/auth/auth.api.ts new file mode 100644 index 0000000..06a60b9 --- /dev/null +++ b/web/src/services/auth/auth.api.ts @@ -0,0 +1,117 @@ +/** + * Auth API Service + * Endpoints de autenticación para ERP Construcción + */ + +import api from '../api'; + +// ============================================================ +// TYPES +// ============================================================ + +export interface LoginCredentials { + email: string; + password: string; +} + +export interface RegisterData { + email: string; + password: string; + firstName: string; + lastName: string; +} + +export interface User { + id: string; + email: string; + firstName: string | null; + lastName: string | null; + tenantId: string; + status: string; + role?: string; + roles?: string[]; +} + +export interface Tenant { + id: string; + name: string; + slug?: string; +} + +export interface AuthResponse { + accessToken: string; + refreshToken: string; + expiresIn?: number; + user: User; + tenant?: Tenant; +} + +export interface RefreshTokenResponse { + accessToken: string; + refreshToken: string; +} + +// ============================================================ +// API CALLS +// ============================================================ + +/** + * Login con email y password + */ +export async function login(credentials: LoginCredentials): Promise { + const response = await api.post('/auth/login', credentials); + return response.data; +} + +/** + * Registro de nuevo usuario + */ +export async function register(data: RegisterData): Promise { + const response = await api.post('/auth/register', data); + return response.data; +} + +/** + * Refresh token + */ +export async function refreshToken(token: string): Promise { + const response = await api.post('/auth/refresh', { + refreshToken: token, + }); + return response.data; +} + +/** + * Logout (invalidar refresh token) + */ +export async function logout(): Promise { + await api.post('/auth/logout'); +} + +/** + * Obtener usuario actual + */ +export async function getCurrentUser(): Promise { + const response = await api.get('/auth/me'); + return response.data; +} + +/** + * Cambiar contraseña + */ +export async function changePassword( + currentPassword: string, + newPassword: string +): Promise { + await api.post('/auth/change-password', { + currentPassword, + newPassword, + }); +} + +/** + * Solicitar reset de contraseña + */ +export async function requestPasswordReset(email: string): Promise { + await api.post('/auth/reset-password', { email }); +} diff --git a/web/src/services/auth/index.ts b/web/src/services/auth/index.ts new file mode 100644 index 0000000..fd15c22 --- /dev/null +++ b/web/src/services/auth/index.ts @@ -0,0 +1 @@ +export * from './auth.api';