((set, get) => ({
user: null,
isAuthenticated: false,
isLoading: false,
error: null,
tokens: null,
setUser: (user) => set({ user, isAuthenticated: !!user }),
setTokens: (tokens) => {
set({ tokens });
if (tokens) {
tokenManager.setTokens(tokens);
} else {
tokenManager.clear();
}
},
login: async (email, password) => {
// Implementación
},
logout: async () => {
set({ user: null, tokens: null, isAuthenticated: false });
tokenManager.clear();
},
// ... otros métodos
}));
```
---
## Hooks Personalizados
### useAuth()
```typescript
function useAuth() {
const store = useAuthStore();
return {
user: store.user,
isAuthenticated: store.isAuthenticated,
isLoading: store.isLoading,
login: store.login,
logout: store.logout,
register: store.register,
};
}
// Uso:
function MyComponent() {
const { user, isAuthenticated, logout } = useAuth();
if (!isAuthenticated) return null;
return Bienvenido {user?.firstName}
;
}
```
### useAuthForm()
```typescript
function useAuthForm(formType: 'login' | 'register' | 'resetPassword') {
const form = useForm({
resolver: zodResolver(getSchema(formType)),
mode: 'onBlur',
});
const onSubmit = async (data) => {
// Validar y enviar
};
return {
form,
isSubmitting: form.formState.isSubmitting,
errors: form.formState.errors,
onSubmit: form.handleSubmit(onSubmit),
};
}
```
### useSessionManager()
```typescript
function useSessionManager() {
const [sessions, setSessions] = useState([]);
const [loading, setLoading] = useState(false);
const loadSessions = async () => {
setLoading(true);
const data = await authService.getSessions();
setSessions(data);
setLoading(false);
};
const revokeSession = async (id: string) => {
await authService.revokeSession(id);
await loadSessions();
};
useEffect(() => {
loadSessions();
}, []);
return { sessions, loading, revokeSession };
}
```
---
## Flujos de Usuario
### Flujo: Registrarse con Email
```
1. Usuario accede a /auth/register
2. Completa formulario:
- Email
- Contraseña (con validación en tiempo real)
- Nombre
- Acepta términos
3. Validación local (Zod)
4. POST /api/v1/auth/register
5. Respuesta: Usuario con status "pending_verification"
6. Redirigir a /auth/verify-email?email=xxx
7. Usuario ingresa código del email
8. POST /api/v1/auth/verify-email
9. Email verificado
10. Redirigir a /auth/login con mensaje "Cuenta verificada"
```
### Flujo: Iniciar Sesión con Email/Contraseña
```
1. Usuario accede a /auth/login
2. Ingresa email y contraseña
3. POST /api/v1/auth/login
4. Respuesta de servidor:
a) Sin 2FA: tokens (accessToken, refreshToken)
b) Con 2FA: tempToken y métodos disponibles
5. Si sin 2FA:
- Guardar tokens en store
- Redirigir a /dashboard
6. Si con 2FA:
- Mostrar formulario de verificación
- Usuario ingresa código
- POST /api/v1/auth/2fa/verify
- Guardar tokens
- Redirigir a /dashboard
```
### Flujo: Iniciar Sesión con OAuth (Google)
```
1. Usuario haz clic en "Iniciar sesión con Google"
2. GET /api/v1/auth/oauth/google/url?redirectTo=/dashboard
3. Respuesta: authUrl + state
4. Redirigir a Google
5. Usuario autentica y otorga permisos
6. Google redirige a /auth/callback/google?code=xxx&state=xxx
7. POST /api/v1/auth/oauth/google { code, state }
8. Respuesta: tokens
9. Guardar en store
10. Redirigir a /dashboard
```
### Flujo: Cambiar Contraseña Olvidada
```
1. Usuario accede a /auth/forgot-password
2. Ingresa email
3. POST /api/v1/auth/forgot-password
4. Mostrar mensaje: "Si la cuenta existe..."
5. Usuario recibe email con link
6. Link dirigido a /auth/reset-password?token=xxx
7. Usuario ingresa nueva contraseña
8. POST /api/v1/auth/reset-password { token, password }
9. Mostrar confirmación
10. Redirigir a /auth/login
```
### Flujo: Activar 2FA (TOTP)
```
1. Usuario va a Settings > Seguridad
2. Haz clic en "Activar autenticación de dos factores"
3. POST /api/v1/auth/2fa/setup
4. Respuesta: QR code + secret
5. Usuario escanea con Google Authenticator/Authy
6. Usuario ingresa código de 6 dígitos
7. POST /api/v1/auth/2fa/enable { code }
8. Respuesta: Backup codes
9. Usuario descargar/copia backup codes
10. Mostrar confirmación: "2FA activado"
```
### Flujo: Iniciar Sesión con Teléfono
```
1. Usuario accede a /auth/phone-login
2. Selecciona país y ingresa teléfono
3. Elige canal: SMS o WhatsApp
4. POST /api/v1/auth/phone/send { phone, channel }
5. Mostrar "Código enviado a +52 555 1234..."
6. Usuario ingresa código de 6 dígitos
7. POST /api/v1/auth/phone/verify { phone, code }
8. Si usuario nuevo: Crear account automáticamente
9. Respuesta: tokens
10. Redirigir a /dashboard
```
---
## Seguridad
### Protección CSRF
```typescript
// Usar state token en OAuth
const state = crypto.randomUUID();
sessionStorage.setItem('oauth_state', state);
// Validar en callback
const urlState = new URLSearchParams(location.search).get('state');
if (urlState !== sessionStorage.getItem('oauth_state')) {
throw new Error('Invalid state token');
}
```
### Manejo de Tokens
```typescript
class TokenManager {
setTokens(tokens: TokenResponse) {
sessionStorage.setItem('accessToken', tokens.accessToken);
localStorage.setItem('refreshToken', tokens.refreshToken);
}
getAccessToken(): string | null {
return sessionStorage.getItem('accessToken');
}
getRefreshToken(): string | null {
return localStorage.getItem('refreshToken');
}
clear() {
sessionStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
}
isTokenExpired(token: string): boolean {
const decoded = jwtDecode(token);
return decoded.exp * 1000 < Date.now();
}
}
```
### Protección XSS
- Usar React (que escapa automáticamente)
- Sanitizar HTML si es necesario: `DOMPurify`
- Content Security Policy headers
### Validación de Inputs
```typescript
const schemas = {
login: z.object({
email: z.string().email('Email inválido'),
password: z.string().min(1, 'Requerido'),
}),
register: z.object({
email: z.string().email('Email inválido'),
password: z.string()
.min(12, 'Mínimo 12 caracteres')
.regex(/[A-Z]/, 'Debe contener mayúscula')
.regex(/[a-z]/, 'Debe contener minúscula')
.regex(/[0-9]/, 'Debe contener número')
.regex(/[^a-zA-Z0-9]/, 'Debe contener carácter especial'),
confirmPassword: z.string(),
}).refine((data) => data.password === data.confirmPassword, {
message: 'Las contraseñas no coinciden',
path: ['confirmPassword'],
}),
};
```
---
## Rutas Protegidas
```typescript
function ProtectedRoute({ children }: { children: ReactNode }) {
const { isAuthenticated, isLoading } = useAuth();
if (isLoading) return ;
if (!isAuthenticated) return ;
return children;
}
// Uso:
} />
} />
```
---
## Testing
### Unit Tests
```typescript
describe('authService.login', () => {
it('debe retornar tokens al login exitoso', async () => {
const result = await authService.login('user@example.com', 'password');
expect(result.data.tokens).toBeDefined();
expect(result.data.user).toBeDefined();
});
it('debe lanzar error con credenciales inválidas', async () => {
await expect(
authService.login('user@example.com', 'wrong')
).rejects.toThrow('INVALID_CREDENTIALS');
});
});
```
### Integration Tests
```typescript
describe('Login Flow', () => {
it('debe completar flujo de login y redirigir', async () => {
render();
await userEvent.type(screen.getByLabelText('Email'), 'user@example.com');
await userEvent.type(screen.getByLabelText('Password'), 'SecurePass123!');
await userEvent.click(screen.getByText('Iniciar sesión'));
await waitFor(() => {
expect(window.location.pathname).toBe('/dashboard');
});
});
});
```
---
## Tipos TypeScript
**Ubicación:** `src/types/auth.ts`
```typescript
export interface AuthUser {
id: string;
email: string;
firstName: string;
lastName: string;
phone?: string;
role: 'investor' | 'admin' | 'support';
status: 'active' | 'pending_verification' | 'suspended';
emailVerified: boolean;
phoneVerified: boolean;
twoFactorEnabled: boolean;
createdAt: Date;
updatedAt: Date;
profile?: {
displayName: string;
avatarUrl?: string;
preferredLanguage: string;
timezone: string;
};
oauthProviders: string[];
}
export interface TokenResponse {
accessToken: string;
refreshToken: string;
expiresIn: number;
tokenType: 'Bearer';
}
export interface LoginResponse {
success: boolean;
data: {
user: AuthUser;
tokens?: TokenResponse;
requires2FA?: boolean;
tempToken?: string;
methods?: string[];
};
}
export interface Session {
id: string;
device: {
type: 'desktop' | 'mobile' | 'tablet';
browser: string;
browserVersion?: string;
os: string;
osVersion: string;
};
location: {
city: string;
country: string;
countryCode: string;
};
ipAddress: string;
lastActivity: Date;
createdAt: Date;
isCurrent: boolean;
}
```
---
## Checklist de Implementación
- [ ] Páginas de autenticación creadas
- [ ] Componentes reutilizables implementados
- [ ] Servicios de API configurados
- [ ] Estado Zustand inicializado
- [ ] Hooks personalizados desarrollados
- [ ] Rutas protegidas implementadas
- [ ] Validaciones con Zod aplicadas
- [ ] Manejo de errores completado
- [ ] Tests unitarios escritos
- [ ] Tests de integración completados
- [ ] Security review realizado
- [ ] Documentación de componentes lista
- [ ] CHANGELOG actualizado
---
## Performance
### Code Splitting
```typescript
const LoginPage = lazy(() => import('./pages/auth/LoginPage'));
const RegisterPage = lazy(() => import('./pages/auth/RegisterPage'));
}>
} />
```
### Memoización
```typescript
const SocialLoginButtons = memo(function SocialLoginButtons(props) {
// Componente
});
```
### Request Caching
```typescript
const { data: user } = useQuery({
queryKey: ['auth', 'me'],
queryFn: () => authService.getCurrentUser(),
staleTime: 5 * 60 * 1000, // 5 minutos
});
```
---
## Referencias
- [ET-AUTH-001: OAuth Specification](./ET-AUTH-001-oauth.md)
- [ET-AUTH-002: JWT Specification](./ET-AUTH-002-jwt.md)
- [ET-AUTH-004: API Endpoints](./ET-AUTH-004-api.md)
- [ET-AUTH-005: Security](./ET-AUTH-005-security.md)
- [React Documentation](https://react.dev)
- [React Router Documentation](https://reactrouter.com)
- [Zustand Documentation](https://github.com/pmndrs/zustand)
- [React Hook Form Documentation](https://react-hook-form.com)
- [Zod Validation Library](https://zod.dev)