From 954da4656c9eb517fa7dc406120070db61dd55e1 Mon Sep 17 00:00:00 2001 From: Adrian Flores Cortes Date: Fri, 30 Jan 2026 15:38:25 -0600 Subject: [PATCH] [Sprint-2] feat: Add hooks for Feature Flags, 2FA, and Audit Created React Query hooks: - useFeatureFlags.ts: Feature flag evaluation and management - use2FA.ts: Two-factor authentication setup/verify/disable - useAuditLogs.ts: Audit logs query with filters and stats Hooks include: - useFlagCheck() for simple feature checks - useFeatures() for multiple flags - use2FAStatus(), useSetup2FA(), useEnable2FA() - useAuditLogs(), useAuditStats(), useSecurityEvents() Co-Authored-By: Claude Opus 4.5 --- src/hooks/useFeatureFlags.ts | 273 +++++++++++++++++++++ src/modules/admin/hooks/useAuditLogs.ts | 306 ++++++++++++++++++++++++ src/modules/auth/hooks/use2FA.ts | 175 ++++++++++++++ 3 files changed, 754 insertions(+) create mode 100644 src/hooks/useFeatureFlags.ts create mode 100644 src/modules/admin/hooks/useAuditLogs.ts create mode 100644 src/modules/auth/hooks/use2FA.ts diff --git a/src/hooks/useFeatureFlags.ts b/src/hooks/useFeatureFlags.ts new file mode 100644 index 0000000..72f3f37 --- /dev/null +++ b/src/hooks/useFeatureFlags.ts @@ -0,0 +1,273 @@ +/** + * useFeatureFlags Hook + * React Query hooks for Feature Flags + * @created Sprint 2 - TASK-2026-01-30-ANALISIS-INTEGRACION + */ + +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { apiClient } from '../lib/apiClient'; + +// ============================================================================ +// Types +// ============================================================================ + +export type FlagStatus = 'disabled' | 'enabled' | 'percentage'; +export type RolloutStage = 'development' | 'beta' | 'production'; + +export interface FeatureFlag { + id: string; + code: string; + name: string; + description: string | null; + category: string; + status: FlagStatus; + rolloutStage: RolloutStage; + rolloutPercentage: number; + defaultValue: boolean; + targetingRules: TargetingRule[]; + metadata: Record; + tags: string[]; + isPermanent: boolean; + expiresAt: Date | null; + createdAt: Date; + updatedAt: Date; +} + +export interface TargetingRule { + type: 'plan' | 'role' | 'user_attribute'; + operator: 'eq' | 'neq' | 'in' | 'not_in' | 'gt' | 'lt'; + attribute?: string; + value?: string; + values?: string[]; +} + +export interface FlagEvaluation { + flagCode: string; + enabled: boolean; + reason: string; +} + +export interface CreateFlagInput { + code: string; + name: string; + description?: string; + category?: string; + status?: FlagStatus; + rolloutPercentage?: number; + defaultValue?: boolean; +} + +// ============================================================================ +// API Functions +// ============================================================================ + +async function getAllFlags(): Promise { + const response = await apiClient.get('/feature-flags'); + return response.data.data; +} + +async function evaluateAllFlags(): Promise { + const response = await apiClient.get('/feature-flags/evaluate'); + return response.data.data; +} + +async function evaluateFlag(code: string): Promise<{ code: string; enabled: boolean }> { + const response = await apiClient.get(`/feature-flags/evaluate/${code}`); + return response.data.data; +} + +async function checkFlag(code: string): Promise { + const response = await apiClient.get(`/feature-flags/check/${code}`); + return response.data.enabled; +} + +async function createFlag(input: CreateFlagInput): Promise { + const response = await apiClient.post('/feature-flags', input); + return response.data.data; +} + +async function toggleFlag(id: string, enabled: boolean): Promise { + const response = await apiClient.post(`/feature-flags/${id}/toggle?enabled=${enabled}`); + return response.data.data; +} + +// ============================================================================ +// React Query Hooks +// ============================================================================ + +/** + * Get all feature flags (admin) + */ +export function useFeatureFlags() { + return useQuery({ + queryKey: ['feature-flags', 'all'], + queryFn: getAllFlags, + staleTime: 5 * 60 * 1000, // 5 minutes + }); +} + +/** + * Evaluate all flags for current user + */ +export function useFlagEvaluations() { + return useQuery({ + queryKey: ['feature-flags', 'evaluations'], + queryFn: evaluateAllFlags, + staleTime: 60 * 1000, // 1 minute + }); +} + +/** + * Evaluate a single flag + */ +export function useFlagEvaluation(code: string) { + return useQuery({ + queryKey: ['feature-flags', 'evaluate', code], + queryFn: () => evaluateFlag(code), + staleTime: 60 * 1000, + enabled: !!code, + }); +} + +/** + * Quick check if flag is enabled + */ +export function useFlagCheck(code: string) { + return useQuery({ + queryKey: ['feature-flags', 'check', code], + queryFn: () => checkFlag(code), + staleTime: 60 * 1000, + enabled: !!code, + }); +} + +/** + * Create a new flag + */ +export function useCreateFlag() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: createFlag, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['feature-flags'] }); + }, + }); +} + +/** + * Toggle flag status + */ +export function useToggleFlag() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ id, enabled }: { id: string; enabled: boolean }) => toggleFlag(id, enabled), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['feature-flags'] }); + }, + }); +} + +// ============================================================================ +// Helper Functions +// ============================================================================ + +/** + * Get status badge color + */ +export function getFlagStatusColor(status: FlagStatus): string { + const colors: Record = { + disabled: 'bg-gray-100 text-gray-800', + enabled: 'bg-green-100 text-green-800', + percentage: 'bg-blue-100 text-blue-800', + }; + return colors[status] || 'bg-gray-100 text-gray-800'; +} + +/** + * Get rollout stage badge color + */ +export function getRolloutStageColor(stage: RolloutStage): string { + const colors: Record = { + development: 'bg-yellow-100 text-yellow-800', + beta: 'bg-purple-100 text-purple-800', + production: 'bg-green-100 text-green-800', + }; + return colors[stage] || 'bg-gray-100 text-gray-800'; +} + +/** + * Get category icon + */ +export function getCategoryIcon(category: string): string { + const icons: Record = { + ui: '🎨', + trading: '📈', + ml: '🤖', + assistant: '💬', + portfolio: '📊', + security: '🔒', + general: '⚙️', + }; + return icons[category] || '⚙️'; +} + +// ============================================================================ +// Custom Hook for Feature Flag Checks +// ============================================================================ + +/** + * Simple hook to check if a feature is enabled + * Usage: const isDarkModeEnabled = useFeature('enable_dark_mode'); + */ +export function useFeature(code: string): boolean { + const { data, isLoading } = useFlagCheck(code); + + // Default to false while loading + if (isLoading || data === undefined) { + return false; + } + + return data; +} + +/** + * Hook to get multiple flag values at once + * Returns a map of code -> enabled + */ +export function useFeatures(): Record { + const { data: evaluations } = useFlagEvaluations(); + + if (!evaluations) { + return {}; + } + + return evaluations.reduce( + (acc, evaluation) => { + acc[evaluation.flagCode] = evaluation.enabled; + return acc; + }, + {} as Record + ); +} + +// ============================================================================ +// Export +// ============================================================================ + +export const featureFlagsHooks = { + useFeatureFlags, + useFlagEvaluations, + useFlagEvaluation, + useFlagCheck, + useCreateFlag, + useToggleFlag, + useFeature, + useFeatures, + getFlagStatusColor, + getRolloutStageColor, + getCategoryIcon, +}; + +export default featureFlagsHooks; diff --git a/src/modules/admin/hooks/useAuditLogs.ts b/src/modules/admin/hooks/useAuditLogs.ts new file mode 100644 index 0000000..b77c216 --- /dev/null +++ b/src/modules/admin/hooks/useAuditLogs.ts @@ -0,0 +1,306 @@ +/** + * useAuditLogs Hook + * React Query hooks for Audit Logs management + * @created Sprint 2 - TASK-2026-01-30-ANALISIS-INTEGRACION + */ + +import { useQuery } from '@tanstack/react-query'; +import { apiClient } from '../../../lib/apiClient'; + +// ============================================================================ +// Types +// ============================================================================ + +export type AuditEventType = + | 'auth.login' + | 'auth.logout' + | 'auth.register' + | 'auth.password_change' + | 'auth.2fa_enabled' + | 'auth.2fa_disabled' + | 'user.profile_update' + | 'user.settings_change' + | 'trading.order_placed' + | 'trading.order_cancelled' + | 'investment.deposit' + | 'investment.withdrawal' + | 'payment.subscription' + | 'payment.refund' + | 'admin.user_suspend' + | 'admin.user_activate' + | 'system.config_change'; + +export type EventSeverity = 'info' | 'warning' | 'error' | 'critical'; + +export type EventStatus = 'success' | 'failure' | 'pending'; + +export interface AuditLog { + id: string; + eventType: AuditEventType; + eventStatus: EventStatus; + severity: EventSeverity; + userId: string | null; + sessionId: string | null; + ipAddress: string | null; + userAgent: string | null; + resourceType: string; + resourceId: string | null; + resourceName: string | null; + action: string; + description: string | null; + oldValues: Record | null; + newValues: Record | null; + metadata: Record; + requestId: string | null; + correlationId: string | null; + serviceName: string | null; + createdAt: Date; +} + +export interface AuditLogFilters { + userId?: string; + eventType?: AuditEventType; + resourceType?: string; + resourceId?: string; + severity?: EventSeverity; + dateFrom?: string; + dateTo?: string; + limit?: number; + offset?: number; +} + +export interface AuditStats { + totalLogs: number; + byEventType: Partial>; + bySeverity: Partial>; + criticalEvents: number; +} + +export interface SecurityEvent { + id: string; + category: string; + severity: EventSeverity; + eventStatus: EventStatus; + userId: string | null; + ipAddress: string; + userAgent: string | null; + eventCode: string; + eventName: string; + description: string | null; + isBlocked: boolean; + blockReason: string | null; + requiresReview: boolean; + createdAt: Date; +} + +export interface SecurityEventFilters { + userId?: string; + category?: string; + severity?: EventSeverity; + isBlocked?: boolean; + requiresReview?: boolean; + dateFrom?: string; + dateTo?: string; + limit?: number; + offset?: number; +} + +// ============================================================================ +// API Functions +// ============================================================================ + +async function getAuditLogs(filters: AuditLogFilters = {}): Promise { + const params = new URLSearchParams(); + + if (filters.userId) params.append('userId', filters.userId); + if (filters.eventType) params.append('eventType', filters.eventType); + if (filters.resourceType) params.append('resourceType', filters.resourceType); + if (filters.resourceId) params.append('resourceId', filters.resourceId); + if (filters.severity) params.append('severity', filters.severity); + if (filters.dateFrom) params.append('dateFrom', filters.dateFrom); + if (filters.dateTo) params.append('dateTo', filters.dateTo); + if (filters.limit) params.append('limit', filters.limit.toString()); + if (filters.offset) params.append('offset', filters.offset.toString()); + + const response = await apiClient.get(`/audit/logs?${params.toString()}`); + return response.data.data || []; +} + +async function getAuditStats(dateFrom?: string, dateTo?: string): Promise { + const params = new URLSearchParams(); + if (dateFrom) params.append('dateFrom', dateFrom); + if (dateTo) params.append('dateTo', dateTo); + + const response = await apiClient.get(`/audit/stats?${params.toString()}`); + return response.data.data; +} + +async function getSecurityEvents(filters: SecurityEventFilters = {}): Promise { + const params = new URLSearchParams(); + + if (filters.userId) params.append('userId', filters.userId); + if (filters.category) params.append('category', filters.category); + if (filters.severity) params.append('severity', filters.severity); + if (filters.isBlocked !== undefined) params.append('isBlocked', filters.isBlocked.toString()); + if (filters.requiresReview !== undefined) params.append('requiresReview', filters.requiresReview.toString()); + if (filters.dateFrom) params.append('dateFrom', filters.dateFrom); + if (filters.dateTo) params.append('dateTo', filters.dateTo); + if (filters.limit) params.append('limit', filters.limit.toString()); + if (filters.offset) params.append('offset', filters.offset.toString()); + + const response = await apiClient.get(`/audit/security-events?${params.toString()}`); + return response.data.data || []; +} + +async function getUserAuditLogs(userId: string, limit = 50): Promise { + return getAuditLogs({ userId, limit }); +} + +// ============================================================================ +// React Query Hooks +// ============================================================================ + +/** + * Get audit logs with optional filters + */ +export function useAuditLogs(filters: AuditLogFilters = {}) { + return useQuery({ + queryKey: ['audit', 'logs', filters], + queryFn: () => getAuditLogs(filters), + staleTime: 30 * 1000, // 30 seconds + }); +} + +/** + * Get audit stats for dashboard + */ +export function useAuditStats(dateFrom?: string, dateTo?: string) { + return useQuery({ + queryKey: ['audit', 'stats', dateFrom, dateTo], + queryFn: () => getAuditStats(dateFrom, dateTo), + staleTime: 60 * 1000, // 1 minute + }); +} + +/** + * Get security events with optional filters + */ +export function useSecurityEvents(filters: SecurityEventFilters = {}) { + return useQuery({ + queryKey: ['audit', 'security-events', filters], + queryFn: () => getSecurityEvents(filters), + staleTime: 30 * 1000, // 30 seconds + }); +} + +/** + * Get audit logs for a specific user + */ +export function useUserAuditLogs(userId: string, limit = 50) { + return useQuery({ + queryKey: ['audit', 'user', userId, limit], + queryFn: () => getUserAuditLogs(userId, limit), + enabled: !!userId, + staleTime: 30 * 1000, + }); +} + +// ============================================================================ +// Helper Functions +// ============================================================================ + +/** + * Get user-friendly label for event type + */ +export function getEventTypeLabel(eventType: AuditEventType): string { + const labels: Record = { + 'auth.login': 'Login', + 'auth.logout': 'Logout', + 'auth.register': 'Registration', + 'auth.password_change': 'Password Change', + 'auth.2fa_enabled': '2FA Enabled', + 'auth.2fa_disabled': '2FA Disabled', + 'user.profile_update': 'Profile Update', + 'user.settings_change': 'Settings Change', + 'trading.order_placed': 'Order Placed', + 'trading.order_cancelled': 'Order Cancelled', + 'investment.deposit': 'Deposit', + 'investment.withdrawal': 'Withdrawal', + 'payment.subscription': 'Subscription', + 'payment.refund': 'Refund', + 'admin.user_suspend': 'User Suspended', + 'admin.user_activate': 'User Activated', + 'system.config_change': 'System Config Change', + }; + return labels[eventType] || eventType; +} + +/** + * Get severity color for styling + */ +export function getSeverityColor(severity: EventSeverity): string { + const colors: Record = { + info: 'text-blue-500', + warning: 'text-yellow-500', + error: 'text-red-500', + critical: 'text-red-700', + }; + return colors[severity] || 'text-gray-500'; +} + +/** + * Get severity badge styles + */ +export function getSeverityBadgeClass(severity: EventSeverity): string { + const classes: Record = { + info: 'bg-blue-100 text-blue-800', + warning: 'bg-yellow-100 text-yellow-800', + error: 'bg-red-100 text-red-800', + critical: 'bg-red-200 text-red-900 font-bold', + }; + return classes[severity] || 'bg-gray-100 text-gray-800'; +} + +/** + * Get status badge styles + */ +export function getStatusBadgeClass(status: EventStatus): string { + const classes: Record = { + success: 'bg-green-100 text-green-800', + failure: 'bg-red-100 text-red-800', + pending: 'bg-yellow-100 text-yellow-800', + }; + return classes[status] || 'bg-gray-100 text-gray-800'; +} + +/** + * Format date for display + */ +export function formatAuditDate(date: Date | string): string { + const d = typeof date === 'string' ? new Date(date) : date; + return d.toLocaleString('es-MX', { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + }); +} + +// ============================================================================ +// Export +// ============================================================================ + +export const auditHooks = { + useAuditLogs, + useAuditStats, + useSecurityEvents, + useUserAuditLogs, + getEventTypeLabel, + getSeverityColor, + getSeverityBadgeClass, + getStatusBadgeClass, + formatAuditDate, +}; + +export default auditHooks; diff --git a/src/modules/auth/hooks/use2FA.ts b/src/modules/auth/hooks/use2FA.ts new file mode 100644 index 0000000..ec04dcf --- /dev/null +++ b/src/modules/auth/hooks/use2FA.ts @@ -0,0 +1,175 @@ +/** + * use2FA Hook + * React Query hooks for Two-Factor Authentication + * @created Sprint 2 - TASK-2026-01-30-ANALISIS-INTEGRACION + */ + +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { apiClient } from '../../../lib/apiClient'; + +// ============================================================================ +// Types +// ============================================================================ + +export interface TwoFactorStatus { + enabled: boolean; + method: '2fa_totp' | null; + backupCodesRemaining: number; +} + +export interface TwoFactorSetupResponse { + secret: string; + qrCodeUrl: string; + backupCodes: string[]; +} + +export interface VerifyCodeInput { + code: string; +} + +export interface RegenerateBackupCodesResponse { + backupCodes: string[]; +} + +// ============================================================================ +// API Functions +// ============================================================================ + +async function get2FAStatus(): Promise { + const response = await apiClient.get('/auth/2fa/status'); + return response.data.data; +} + +async function setup2FA(): Promise { + const response = await apiClient.post('/auth/2fa/setup'); + return response.data.data; +} + +async function enable2FA(code: string): Promise<{ message: string }> { + const response = await apiClient.post('/auth/2fa/enable', { code }); + return response.data; +} + +async function disable2FA(code: string): Promise<{ message: string }> { + const response = await apiClient.post('/auth/2fa/disable', { code }); + return response.data; +} + +async function regenerateBackupCodes(code: string): Promise { + const response = await apiClient.post('/auth/2fa/backup-codes', { code }); + return response.data.data; +} + +// ============================================================================ +// React Query Hooks +// ============================================================================ + +/** + * Get 2FA status for current user + */ +export function use2FAStatus() { + return useQuery({ + queryKey: ['2fa', 'status'], + queryFn: get2FAStatus, + staleTime: 5 * 60 * 1000, // 5 minutes + }); +} + +/** + * Setup 2FA - generates secret, QR code, and backup codes + */ +export function useSetup2FA() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: setup2FA, + onSuccess: () => { + // Invalidate status after setup + queryClient.invalidateQueries({ queryKey: ['2fa', 'status'] }); + }, + }); +} + +/** + * Enable 2FA with verification code + */ +export function useEnable2FA() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (code: string) => enable2FA(code), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['2fa', 'status'] }); + }, + }); +} + +/** + * Disable 2FA with verification code + */ +export function useDisable2FA() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (code: string) => disable2FA(code), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['2fa', 'status'] }); + }, + }); +} + +/** + * Regenerate backup codes with current TOTP code + */ +export function useRegenerateBackupCodes() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (code: string) => regenerateBackupCodes(code), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['2fa', 'status'] }); + }, + }); +} + +// ============================================================================ +// Helper Functions +// ============================================================================ + +/** + * Format backup code for display (e.g., "ABCD-1234") + */ +export function formatBackupCode(code: string): string { + if (code.includes('-')) return code; + if (code.length === 8) { + return `${code.slice(0, 4)}-${code.slice(4)}`; + } + return code; +} + +/** + * Get user-friendly 2FA status label + */ +export function get2FAStatusLabel(status: TwoFactorStatus | undefined): string { + if (!status) return 'Loading...'; + if (status.enabled) { + return `Enabled (${status.backupCodesRemaining} backup codes remaining)`; + } + return 'Not enabled'; +} + +// ============================================================================ +// Export +// ============================================================================ + +export const twoFactorHooks = { + use2FAStatus, + useSetup2FA, + useEnable2FA, + useDisable2FA, + useRegenerateBackupCodes, + formatBackupCode, + get2FAStatusLabel, +}; + +export default twoFactorHooks;