diff --git a/app/_layout.tsx b/app/_layout.tsx index de72729..81834ea 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -1,9 +1,10 @@ import React, { useEffect, useState } from 'react'; import { View } from 'react-native'; import { Stack, useRouter, useSegments } from 'expo-router'; -import { PaperProvider, MD3DarkTheme, ActivityIndicator, Snackbar } from 'react-native-paper'; +import { PaperProvider, MD3DarkTheme, ActivityIndicator, Snackbar, Button, Text } from 'react-native-paper'; import { SafeAreaProvider } from 'react-native-safe-area-context'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; +import * as LocalAuthentication from 'expo-local-authentication'; import { useAppStore } from '../src/store/appStore'; import { registerToastCallback } from '../src/utils/errorHandler'; @@ -36,7 +37,7 @@ const APP_THEME = { export default function RootLayout() { const router = useRouter(); const segments = useSegments(); - const { config, loadConfig } = useAppStore(); + const { config, preferences, isAuthenticated, loadConfig, setAuthenticated } = useAppStore(); const [isReady, setIsReady] = useState(false); const [toastMessage, setToastMessage] = useState(''); const [toastVisible, setToastVisible] = useState(false); @@ -52,25 +53,44 @@ export default function RootLayout() { }); }, []); - /** Charge la configuration depuis le secure store puis marque l'app comme prête */ + const runBiometricAuth = async (): Promise => { + const result = await LocalAuthentication.authenticateAsync({ + promptMessage: 'Authentifiez-vous pour accéder à n8n Pilot', + cancelLabel: 'Annuler', + }); + return result.success; + }; + useEffect(() => { - loadConfig().finally(() => setIsReady(true)); + const init = async () => { + await loadConfig(); + const { preferences: prefs } = useAppStore.getState(); + if (prefs.biometricEnabled) { + const success = await runBiometricAuth(); + setAuthenticated(success); + } else { + setAuthenticated(true); + } + setIsReady(true); + }; + init(); }, []); - /** - * Gère la redirection initiale selon l'état de configuration. - * - Pas configuré et hors /setup → redirige vers /setup - * - Configuré et dans /setup → redirige vers les tabs - */ + const handleUnlock = async (): Promise => { + const success = await runBiometricAuth(); + if (success) setAuthenticated(true); + }; + useEffect(() => { if (!isReady) return; + if (preferences.biometricEnabled && !isAuthenticated) return; const inSetup = segments[0] === 'setup'; if (!config.isConfigured && !inSetup) { router.replace('/setup'); } else if (config.isConfigured && inSetup) { router.replace('/(tabs)'); } - }, [isReady, config.isConfigured, segments]); + }, [isReady, config.isConfigured, isAuthenticated, preferences.biometricEnabled, segments]); if (!isReady) { return ( @@ -80,6 +100,28 @@ export default function RootLayout() { ); } + if (preferences.biometricEnabled && !isAuthenticated) { + return ( + + + + + + n8n Pilot + + + Authentification biométrique requise + + + + + + + ); + } + return ( diff --git a/src/api/client.ts b/src/api/client.ts index f6798d4..3dddefa 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -15,6 +15,7 @@ export const SECURE_STORE_KEYS = { BASE_URL: 'n8n_base_url', API_KEY: 'n8n_api_key', APP_TOKEN: 'n8n_app_token', + BIOMETRIC_ENABLED: 'n8n_biometric_enabled', } as const; /** diff --git a/src/api/executions.ts b/src/api/executions.ts index 22433c2..5e482ee 100644 --- a/src/api/executions.ts +++ b/src/api/executions.ts @@ -93,7 +93,9 @@ export const fetchExecutions = async ( * @returns Exécution complète avec données de nœuds */ export const fetchExecutionById = async (id: string): Promise => { - const response = await apiClient.get(`/api/v1/executions/${id}`); + const response = await apiClient.get(`/api/v1/executions/${id}`, { + params: { includeData: true }, + }); return response.data; }; diff --git a/src/hooks/useWorkflows.ts b/src/hooks/useWorkflows.ts index abc86a8..8ceea8f 100644 --- a/src/hooks/useWorkflows.ts +++ b/src/hooks/useWorkflows.ts @@ -96,11 +96,7 @@ export const useWorkflows = (): UseWorkflowsResult => { }, []); const triggerWorkflow = useCallback(async (id: string): Promise => { - try { - await runWorkflow(id); - } catch { - // Géré par l'intercepteur Axios - } + await runWorkflow(id); }, []); return { diff --git a/src/screens/SettingsScreen.tsx b/src/screens/SettingsScreen.tsx index 91e74f2..a698659 100644 --- a/src/screens/SettingsScreen.tsx +++ b/src/screens/SettingsScreen.tsx @@ -122,6 +122,7 @@ const SettingsScreen: React.FC = () => { SecureStore.deleteItemAsync(SECURE_STORE_KEYS.BASE_URL), SecureStore.deleteItemAsync(SECURE_STORE_KEYS.API_KEY), SecureStore.deleteItemAsync(SECURE_STORE_KEYS.APP_TOKEN), + SecureStore.deleteItemAsync(SECURE_STORE_KEYS.BIOMETRIC_ENABLED), ]); setBaseUrl(''); setApiKey(''); diff --git a/src/screens/WorkflowsScreen.tsx b/src/screens/WorkflowsScreen.tsx index d07360b..69c2815 100644 --- a/src/screens/WorkflowsScreen.tsx +++ b/src/screens/WorkflowsScreen.tsx @@ -23,8 +23,12 @@ const WorkflowsScreen: React.FC = () => { * @param id - Identifiant du workflow à lancer */ const handleRun = async (id: string): Promise => { - await triggerWorkflow(id); - setSnackMessage('Workflow déclenché avec succès'); + try { + await triggerWorkflow(id); + setSnackMessage('Exécution déclenchée'); + } catch { + // L'intercepteur Axios a déjà affiché le toast d'erreur + } }; /** diff --git a/src/store/appStore.ts b/src/store/appStore.ts index b6454c5..dae648b 100644 --- a/src/store/appStore.ts +++ b/src/store/appStore.ts @@ -83,23 +83,36 @@ export const useAppStore = create((set) => ({ loadConfig: async () => { set({ isLoading: true }); try { - // Seule l'URL est lue dans le store visible — les secrets restent dans le secure store - const baseUrl = await SecureStore.getItemAsync(SECURE_STORE_KEYS.BASE_URL); + const [baseUrl, biometricRaw] = await Promise.all([ + SecureStore.getItemAsync(SECURE_STORE_KEYS.BASE_URL), + SecureStore.getItemAsync(SECURE_STORE_KEYS.BIOMETRIC_ENABLED), + ]); set({ config: { baseUrl: baseUrl ?? '', isConfigured: !!baseUrl, }, + preferences: { + pollingInterval: 30_000, + biometricEnabled: biometricRaw === 'true', + }, }); } finally { set({ isLoading: false }); } }, - setPreferences: (prefs) => + setPreferences: (prefs) => { + if ('biometricEnabled' in prefs) { + SecureStore.setItemAsync( + SECURE_STORE_KEYS.BIOMETRIC_ENABLED, + prefs.biometricEnabled ? 'true' : 'false' + ).catch(() => {}); + } set((state) => ({ preferences: { ...state.preferences, ...prefs }, - })), + })); + }, setAuthenticated: (auth) => set({ isAuthenticated: auth }), }));