feat: add MFA types, hooks, and APP_MFA_REQUIRED interceptor
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -27,6 +27,14 @@ async function apiFetch<T>(path: string, options: RequestInit = {}): Promise<T>
|
|||||||
throw new Error('Unauthorized');
|
throw new Error('Unauthorized');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (response.status === 403) {
|
||||||
|
const errorHeader = response.headers.get('X-Cameleer-Error');
|
||||||
|
if (errorHeader === 'APP_MFA_REQUIRED') {
|
||||||
|
window.location.href = '/platform/tenant/settings?mfa=required';
|
||||||
|
throw new Error('MFA enrollment required');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const text = await response.text();
|
const text = await response.text();
|
||||||
throw new Error(`API error ${response.status}: ${text}`);
|
throw new Error(`API error ${response.status}: ${text}`);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { api } from './client';
|
import { api } from './client';
|
||||||
import type { DashboardData, TenantLicenseData, TenantSettings, AuditLogPage, AuditLogFilters, SsoConnector, CreateSsoConnectorRequest, SsoTestResult } from '../types/api';
|
import type { DashboardData, TenantLicenseData, TenantSettings, AuditLogPage, AuditLogFilters, SsoConnector, CreateSsoConnectorRequest, SsoTestResult, MfaStatus, MfaSetupResponse, BackupCodesResponse } from '../types/api';
|
||||||
|
|
||||||
export function useTenantDashboard() {
|
export function useTenantDashboard() {
|
||||||
return useQuery<DashboardData>({
|
return useQuery<DashboardData>({
|
||||||
@@ -143,3 +143,57 @@ export function useTenantAuditLog(filters: Omit<AuditLogFilters, 'tenantId'>) {
|
|||||||
queryFn: () => api.get(`/tenant/audit?${params.toString()}`),
|
queryFn: () => api.get(`/tenant/audit?${params.toString()}`),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MFA hooks
|
||||||
|
export function useMfaStatus() {
|
||||||
|
return useQuery<MfaStatus>({
|
||||||
|
queryKey: ['tenant', 'mfa', 'status'],
|
||||||
|
queryFn: () => api.get('/tenant/mfa/status'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useMfaSetup() {
|
||||||
|
return useMutation<MfaSetupResponse, Error, void>({
|
||||||
|
mutationFn: () => api.post('/tenant/mfa/totp/setup'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useMfaVerify() {
|
||||||
|
const qc = useQueryClient();
|
||||||
|
return useMutation<{ verified: boolean }, Error, { secret: string; code: string }>({
|
||||||
|
mutationFn: (body) => api.post('/tenant/mfa/totp/verify', body),
|
||||||
|
onSuccess: () => qc.invalidateQueries({ queryKey: ['tenant', 'mfa'] }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useMfaBackupCodes() {
|
||||||
|
const qc = useQueryClient();
|
||||||
|
return useMutation<BackupCodesResponse, Error, void>({
|
||||||
|
mutationFn: () => api.post('/tenant/mfa/backup-codes'),
|
||||||
|
onSuccess: () => qc.invalidateQueries({ queryKey: ['tenant', 'mfa'] }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useMfaRemove() {
|
||||||
|
const qc = useQueryClient();
|
||||||
|
return useMutation<void, Error, void>({
|
||||||
|
mutationFn: () => api.delete('/tenant/mfa/totp'),
|
||||||
|
onSuccess: () => qc.invalidateQueries({ queryKey: ['tenant', 'mfa'] }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useResetTeamMemberMfa() {
|
||||||
|
const qc = useQueryClient();
|
||||||
|
return useMutation<void, Error, string>({
|
||||||
|
mutationFn: (userId) => api.delete(`/tenant/users/${userId}/mfa`),
|
||||||
|
onSuccess: () => qc.invalidateQueries({ queryKey: ['tenant', 'team'] }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUpdateTenantSettings() {
|
||||||
|
const qc = useQueryClient();
|
||||||
|
return useMutation<void, Error, Record<string, unknown>>({
|
||||||
|
mutationFn: (updates) => api.patch('/tenant/settings', updates),
|
||||||
|
onSuccess: () => qc.invalidateQueries({ queryKey: ['tenant', 'settings'] }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ export interface TenantSettings {
|
|||||||
status: string;
|
status: string;
|
||||||
serverEndpoint: string | null;
|
serverEndpoint: string | null;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
|
mfaRequired?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// SSO connector types
|
// SSO connector types
|
||||||
@@ -200,3 +201,18 @@ export interface TenantMetricsEntry {
|
|||||||
serverState: string;
|
serverState: string;
|
||||||
metrics: MetricsSummary | null;
|
metrics: MetricsSummary | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MFA types
|
||||||
|
export interface MfaStatus {
|
||||||
|
enrolled: boolean;
|
||||||
|
hasBackupCodes: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MfaSetupResponse {
|
||||||
|
secret: string;
|
||||||
|
secretQrCode: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BackupCodesResponse {
|
||||||
|
codes: string[];
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user