[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 <noreply@anthropic.com>
This commit is contained in:
Adrian Flores Cortes 2026-01-30 15:38:25 -06:00
parent 6d0673a799
commit 954da4656c
3 changed files with 754 additions and 0 deletions

View File

@ -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<string, unknown>;
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<FeatureFlag[]> {
const response = await apiClient.get('/feature-flags');
return response.data.data;
}
async function evaluateAllFlags(): Promise<FlagEvaluation[]> {
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<boolean> {
const response = await apiClient.get(`/feature-flags/check/${code}`);
return response.data.enabled;
}
async function createFlag(input: CreateFlagInput): Promise<FeatureFlag> {
const response = await apiClient.post('/feature-flags', input);
return response.data.data;
}
async function toggleFlag(id: string, enabled: boolean): Promise<FeatureFlag> {
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<FlagStatus, string> = {
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<RolloutStage, string> = {
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<string, string> = {
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<string, boolean> {
const { data: evaluations } = useFlagEvaluations();
if (!evaluations) {
return {};
}
return evaluations.reduce(
(acc, evaluation) => {
acc[evaluation.flagCode] = evaluation.enabled;
return acc;
},
{} as Record<string, boolean>
);
}
// ============================================================================
// Export
// ============================================================================
export const featureFlagsHooks = {
useFeatureFlags,
useFlagEvaluations,
useFlagEvaluation,
useFlagCheck,
useCreateFlag,
useToggleFlag,
useFeature,
useFeatures,
getFlagStatusColor,
getRolloutStageColor,
getCategoryIcon,
};
export default featureFlagsHooks;

View File

@ -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<string, unknown> | null;
newValues: Record<string, unknown> | null;
metadata: Record<string, unknown>;
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<Record<AuditEventType, number>>;
bySeverity: Partial<Record<EventSeverity, number>>;
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<AuditLog[]> {
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<AuditStats> {
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<SecurityEvent[]> {
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<AuditLog[]> {
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<AuditEventType, string> = {
'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<EventSeverity, string> = {
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<EventSeverity, string> = {
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<EventStatus, string> = {
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;

View File

@ -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<TwoFactorStatus> {
const response = await apiClient.get('/auth/2fa/status');
return response.data.data;
}
async function setup2FA(): Promise<TwoFactorSetupResponse> {
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<RegenerateBackupCodesResponse> {
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;