[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:
parent
6d0673a799
commit
954da4656c
273
src/hooks/useFeatureFlags.ts
Normal file
273
src/hooks/useFeatureFlags.ts
Normal 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;
|
||||
306
src/modules/admin/hooks/useAuditLogs.ts
Normal file
306
src/modules/admin/hooks/useAuditLogs.ts
Normal 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;
|
||||
175
src/modules/auth/hooks/use2FA.ts
Normal file
175
src/modules/auth/hooks/use2FA.ts
Normal 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;
|
||||
Loading…
Reference in New Issue
Block a user