diff --git a/apps/web/.env.example b/apps/web/.env.example
new file mode 100644
index 0000000..0ce6b13
--- /dev/null
+++ b/apps/web/.env.example
@@ -0,0 +1 @@
+NEXT_PUBLIC_API_URL=http://localhost:4000/api
diff --git a/apps/web/app/(auth)/layout.tsx b/apps/web/app/(auth)/layout.tsx
new file mode 100644
index 0000000..c19e053
--- /dev/null
+++ b/apps/web/app/(auth)/layout.tsx
@@ -0,0 +1,11 @@
+export default function AuthLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ return (
+
+ );
+}
diff --git a/apps/web/app/(auth)/login/page.tsx b/apps/web/app/(auth)/login/page.tsx
new file mode 100644
index 0000000..2ef4133
--- /dev/null
+++ b/apps/web/app/(auth)/login/page.tsx
@@ -0,0 +1,90 @@
+'use client';
+
+import { useState } from 'react';
+import { useRouter } from 'next/navigation';
+import Link from 'next/link';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
+import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
+import { login } from '@/lib/api/auth';
+import { useAuthStore } from '@/stores/auth-store';
+
+export default function LoginPage() {
+ const router = useRouter();
+ const { setUser, setTokens } = useAuthStore();
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState('');
+
+ async function handleSubmit(e: React.FormEvent) {
+ e.preventDefault();
+ setIsLoading(true);
+ setError('');
+
+ const formData = new FormData(e.currentTarget);
+ const email = formData.get('email') as string;
+ const password = formData.get('password') as string;
+
+ try {
+ const response = await login({ email, password });
+ setTokens(response.accessToken, response.refreshToken);
+ setUser(response.user);
+ router.push('/dashboard');
+ } catch (err: any) {
+ setError(err.response?.data?.message || 'Error al iniciar sesión');
+ } finally {
+ setIsLoading(false);
+ }
+ }
+
+ return (
+
+
+ Iniciar Sesión
+
+ Ingresa tus credenciales para acceder a tu cuenta
+
+
+
+
+ );
+}
diff --git a/apps/web/app/(auth)/register/page.tsx b/apps/web/app/(auth)/register/page.tsx
new file mode 100644
index 0000000..bd4d1fe
--- /dev/null
+++ b/apps/web/app/(auth)/register/page.tsx
@@ -0,0 +1,119 @@
+'use client';
+
+import { useState } from 'react';
+import { useRouter } from 'next/navigation';
+import Link from 'next/link';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
+import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
+import { register } from '@/lib/api/auth';
+import { useAuthStore } from '@/stores/auth-store';
+
+export default function RegisterPage() {
+ const router = useRouter();
+ const { setUser, setTokens } = useAuthStore();
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState('');
+
+ async function handleSubmit(e: React.FormEvent) {
+ e.preventDefault();
+ setIsLoading(true);
+ setError('');
+
+ const formData = new FormData(e.currentTarget);
+
+ try {
+ const response = await register({
+ empresa: {
+ nombre: formData.get('empresaNombre') as string,
+ rfc: formData.get('empresaRfc') as string,
+ },
+ usuario: {
+ nombre: formData.get('nombre') as string,
+ email: formData.get('email') as string,
+ password: formData.get('password') as string,
+ },
+ });
+ setTokens(response.accessToken, response.refreshToken);
+ setUser(response.user);
+ router.push('/dashboard');
+ } catch (err: any) {
+ setError(err.response?.data?.message || 'Error al registrarse');
+ } finally {
+ setIsLoading(false);
+ }
+ }
+
+ return (
+
+
+ Crear Cuenta
+
+ Registra tu empresa y comienza tu prueba gratuita
+
+
+
+
+ );
+}
diff --git a/apps/web/lib/api/auth.ts b/apps/web/lib/api/auth.ts
new file mode 100644
index 0000000..d23084a
--- /dev/null
+++ b/apps/web/lib/api/auth.ts
@@ -0,0 +1,22 @@
+import { apiClient } from './client';
+import type { LoginRequest, RegisterRequest, LoginResponse } from '@horux/shared';
+
+export async function login(data: LoginRequest): Promise {
+ const response = await apiClient.post('/auth/login', data);
+ return response.data;
+}
+
+export async function register(data: RegisterRequest): Promise {
+ const response = await apiClient.post('/auth/register', data);
+ return response.data;
+}
+
+export async function logout(): Promise {
+ const refreshToken = localStorage.getItem('refreshToken');
+ await apiClient.post('/auth/logout', { refreshToken });
+}
+
+export async function getMe(): Promise {
+ const response = await apiClient.get('/auth/me');
+ return response.data.user;
+}
diff --git a/apps/web/lib/api/client.ts b/apps/web/lib/api/client.ts
new file mode 100644
index 0000000..f2ffea0
--- /dev/null
+++ b/apps/web/lib/api/client.ts
@@ -0,0 +1,52 @@
+import axios from 'axios';
+
+export const apiClient = axios.create({
+ baseURL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000/api',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+});
+
+apiClient.interceptors.request.use((config) => {
+ if (typeof window !== 'undefined') {
+ const token = localStorage.getItem('accessToken');
+ if (token) {
+ config.headers.Authorization = `Bearer ${token}`;
+ }
+ }
+ return config;
+});
+
+apiClient.interceptors.response.use(
+ (response) => response,
+ async (error) => {
+ const originalRequest = error.config;
+
+ if (error.response?.status === 401 && !originalRequest._retry) {
+ originalRequest._retry = true;
+
+ try {
+ const refreshToken = localStorage.getItem('refreshToken');
+ if (refreshToken) {
+ const response = await axios.post(
+ `${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000/api'}/auth/refresh`,
+ { refreshToken }
+ );
+
+ const { accessToken, refreshToken: newRefreshToken } = response.data;
+ localStorage.setItem('accessToken', accessToken);
+ localStorage.setItem('refreshToken', newRefreshToken);
+
+ originalRequest.headers.Authorization = `Bearer ${accessToken}`;
+ return apiClient(originalRequest);
+ }
+ } catch {
+ localStorage.removeItem('accessToken');
+ localStorage.removeItem('refreshToken');
+ window.location.href = '/login';
+ }
+ }
+
+ return Promise.reject(error);
+ }
+);
diff --git a/apps/web/stores/auth-store.ts b/apps/web/stores/auth-store.ts
new file mode 100644
index 0000000..f648284
--- /dev/null
+++ b/apps/web/stores/auth-store.ts
@@ -0,0 +1,34 @@
+import { create } from 'zustand';
+import { persist } from 'zustand/middleware';
+import type { UserInfo } from '@horux/shared';
+
+interface AuthState {
+ user: UserInfo | null;
+ isAuthenticated: boolean;
+ setUser: (user: UserInfo | null) => void;
+ setTokens: (accessToken: string, refreshToken: string) => void;
+ logout: () => void;
+}
+
+export const useAuthStore = create()(
+ persist(
+ (set) => ({
+ user: null,
+ isAuthenticated: false,
+ setUser: (user) => set({ user, isAuthenticated: !!user }),
+ setTokens: (accessToken, refreshToken) => {
+ localStorage.setItem('accessToken', accessToken);
+ localStorage.setItem('refreshToken', refreshToken);
+ },
+ logout: () => {
+ localStorage.removeItem('accessToken');
+ localStorage.removeItem('refreshToken');
+ set({ user: null, isAuthenticated: false });
+ },
+ }),
+ {
+ name: 'horux-auth',
+ partialize: (state) => ({ user: state.user, isAuthenticated: state.isAuthenticated }),
+ }
+ )
+);