chore: build
This commit is contained in:
@@ -12,6 +12,7 @@ import { ThemeProvider, useTheme } from "./src/context/ThemeContext";
|
||||
|
||||
import HomeScreen from "./src/screens/HomeScreen";
|
||||
import ClientLoginScreen from "./src/screens/auth/ClientLoginScreen";
|
||||
import ClientTwoFAScreen from "./src/screens/auth/ClientTwoFAScreen";
|
||||
import ChangePasswordScreen from "./src/screens/auth/ChangePasswordScreen";
|
||||
import ClientNavigator from "./src/navigation/ClientNavigator";
|
||||
import type { RootStackParamList, ChangePasswordStackParamList } from "./src/navigation/types";
|
||||
@@ -39,6 +40,11 @@ function AuthStack() {
|
||||
component={ClientLoginScreen}
|
||||
options={{ title: "Connexion" }}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="clientTwoFA"
|
||||
component={ClientTwoFAScreen}
|
||||
options={{ title: "Vérification" }}
|
||||
/>
|
||||
</Stack.Navigator>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -27,6 +27,8 @@ export interface AuthResponse {
|
||||
access_token?: string;
|
||||
token_type?: string;
|
||||
expires_in?: number;
|
||||
requires_2fa?: boolean;
|
||||
session_token?: string;
|
||||
user?: {
|
||||
id: number;
|
||||
username: string;
|
||||
@@ -48,6 +50,13 @@ export const loginUser = async (
|
||||
username,
|
||||
password,
|
||||
});
|
||||
if (data.requires_2fa) {
|
||||
return {
|
||||
success: false,
|
||||
requires_2fa: true,
|
||||
session_token: data.session_token,
|
||||
};
|
||||
}
|
||||
if (!data.access_token) {
|
||||
return { success: false, message: "Token non reçu du serveur" };
|
||||
}
|
||||
@@ -68,6 +77,35 @@ export const loginUser = async (
|
||||
}
|
||||
};
|
||||
|
||||
export const verifyClient2FA = async (
|
||||
sessionToken: string,
|
||||
code: string,
|
||||
): Promise<AuthResponse> => {
|
||||
try {
|
||||
const { data } = await apiClient.post(`${V1}/auth/2fa/verify`, {
|
||||
session_token: sessionToken,
|
||||
code,
|
||||
});
|
||||
if (!data.access_token) {
|
||||
return { success: false, message: "Token non reçu du serveur" };
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
message: "Connexion réussie",
|
||||
access_token: data.access_token,
|
||||
token_type: data.token_type,
|
||||
expires_in: data.expires_in,
|
||||
user: data.user,
|
||||
};
|
||||
} catch (error: any) {
|
||||
const msg =
|
||||
error.response?.data?.error ||
|
||||
error.message ||
|
||||
"Code invalide";
|
||||
return { success: false, message: msg };
|
||||
}
|
||||
};
|
||||
|
||||
export const registerUser = async (
|
||||
username: string,
|
||||
password: string,
|
||||
|
||||
@@ -3,6 +3,7 @@ export type RootStackParamList = {
|
||||
login: undefined;
|
||||
register: undefined;
|
||||
role: undefined;
|
||||
clientTwoFA: { sessionToken: string };
|
||||
};
|
||||
|
||||
export type ChangePasswordStackParamList = {
|
||||
|
||||
@@ -61,7 +61,11 @@ const LoginClient = () => {
|
||||
formData.username,
|
||||
formData.password,
|
||||
);
|
||||
if (result.success && result.access_token) {
|
||||
if (result.requires_2fa && result.session_token) {
|
||||
navigation.navigate("clientTwoFA", {
|
||||
sessionToken: result.session_token,
|
||||
});
|
||||
} else if (result.success && result.access_token) {
|
||||
await loginClient(result.access_token, result.user?.must_change_password ?? false);
|
||||
} else {
|
||||
const errorMessage =
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
import React, { useState, useMemo } from "react";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
ActivityIndicator,
|
||||
StyleSheet,
|
||||
KeyboardAvoidingView,
|
||||
ScrollView,
|
||||
} from "react-native";
|
||||
import { Feather } from "@expo/vector-icons";
|
||||
import { verifyClient2FA } from "../../api/api";
|
||||
import { useAuth } from "../../auth/AuthContext";
|
||||
import { useNavigation, useRoute, type RouteProp } from "@react-navigation/native";
|
||||
import type { NativeStackNavigationProp } from "@react-navigation/native-stack";
|
||||
import type { RootStackParamList } from "../../navigation/types";
|
||||
import { useTheme } from "../../context/ThemeContext";
|
||||
|
||||
type TwoFANavigationProp = NativeStackNavigationProp<
|
||||
RootStackParamList,
|
||||
"clientTwoFA"
|
||||
>;
|
||||
type TwoFARouteProp = RouteProp<RootStackParamList, "clientTwoFA">;
|
||||
|
||||
const ClientTwoFAScreen = () => {
|
||||
const navigation = useNavigation<TwoFANavigationProp>();
|
||||
const route = useRoute<TwoFARouteProp>();
|
||||
const { sessionToken } = route.params;
|
||||
const { loginClient } = useAuth();
|
||||
const { colors } = useTheme();
|
||||
|
||||
const [code, setCode] = useState("");
|
||||
const [apiError, setApiError] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (code.trim().length !== 6) {
|
||||
setApiError("Le code doit contenir 6 chiffres");
|
||||
return;
|
||||
}
|
||||
setIsLoading(true);
|
||||
setApiError("");
|
||||
try {
|
||||
const result = await verifyClient2FA(sessionToken, code.trim());
|
||||
if (result.success && result.access_token) {
|
||||
await loginClient(result.access_token, result.user?.must_change_password ?? false);
|
||||
} else {
|
||||
setApiError(result.message || "Code invalide");
|
||||
}
|
||||
} catch (err) {
|
||||
const message =
|
||||
err instanceof Error ? err.message : "Erreur de connexion";
|
||||
setApiError(message);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleChangeCode = (value: string) => {
|
||||
const digitsOnly = value.replace(/[^0-9]/g, "").slice(0, 6);
|
||||
setCode(digitsOnly);
|
||||
if (apiError) setApiError("");
|
||||
};
|
||||
|
||||
const styles = useMemo(
|
||||
() =>
|
||||
StyleSheet.create({
|
||||
container: { flex: 1, backgroundColor: colors.bgSecondary },
|
||||
scrollContent: {
|
||||
flexGrow: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
padding: 16,
|
||||
},
|
||||
card: {
|
||||
width: "100%",
|
||||
maxWidth: 400,
|
||||
borderRadius: 16,
|
||||
padding: 24,
|
||||
backgroundColor: colors.bgPrimary,
|
||||
},
|
||||
header: { alignItems: "center", marginBottom: 24 },
|
||||
title: {
|
||||
fontSize: 24,
|
||||
fontWeight: "bold",
|
||||
marginTop: 8,
|
||||
color: colors.textPrimary,
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: 14,
|
||||
marginTop: 4,
|
||||
textAlign: "center",
|
||||
color: colors.textMuted,
|
||||
},
|
||||
apiError: {
|
||||
backgroundColor: "#fee2e2",
|
||||
color: "#991b1b",
|
||||
padding: 12,
|
||||
borderRadius: 6,
|
||||
marginBottom: 16,
|
||||
textAlign: "center",
|
||||
},
|
||||
inputGroup: { marginBottom: 16 },
|
||||
label: { marginBottom: 4, fontWeight: "500", color: colors.textSecondary },
|
||||
inputWrapper: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
borderWidth: 1,
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 12,
|
||||
borderColor: colors.border,
|
||||
backgroundColor: colors.bgInput,
|
||||
},
|
||||
icon: { marginRight: 8 },
|
||||
input: {
|
||||
flex: 1,
|
||||
height: 48,
|
||||
color: colors.textPrimary,
|
||||
fontSize: 20,
|
||||
letterSpacing: 8,
|
||||
textAlign: "center",
|
||||
},
|
||||
submitButton: {
|
||||
backgroundColor: colors.accent,
|
||||
borderRadius: 8,
|
||||
padding: 12,
|
||||
alignItems: "center",
|
||||
marginTop: 8,
|
||||
},
|
||||
submitText: { color: colors.white, fontWeight: "600", fontSize: 16 },
|
||||
backButton: { alignItems: "center", marginTop: 16 },
|
||||
backText: { color: colors.textMuted, fontSize: 14 },
|
||||
}),
|
||||
[colors],
|
||||
);
|
||||
|
||||
return (
|
||||
<KeyboardAvoidingView style={styles.container} behavior="padding">
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<View style={styles.card}>
|
||||
<View style={styles.header}>
|
||||
<Feather name="shield" size={48} color={colors.accent} />
|
||||
<Text style={styles.title}>Vérification en deux étapes</Text>
|
||||
<Text style={styles.subtitle}>
|
||||
Entrez le code à 6 chiffres envoyé sur Telegram
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{apiError ? (
|
||||
<Text style={styles.apiError}>⚠️ {apiError}</Text>
|
||||
) : null}
|
||||
|
||||
<View style={styles.inputGroup}>
|
||||
<Text style={styles.label}>Code de vérification</Text>
|
||||
<View style={styles.inputWrapper}>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="000000"
|
||||
placeholderTextColor={colors.textMuted}
|
||||
value={code}
|
||||
onChangeText={handleChangeCode}
|
||||
editable={!isLoading}
|
||||
keyboardType="number-pad"
|
||||
maxLength={6}
|
||||
autoFocus
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.submitButton, isLoading && { opacity: 0.6 }]}
|
||||
onPress={handleSubmit}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<ActivityIndicator color={colors.white} />
|
||||
) : (
|
||||
<Text style={styles.submitText}>Vérifier</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={styles.backButton}
|
||||
onPress={() => navigation.goBack()}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<Text style={styles.backText}>Retour à la connexion</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClientTwoFAScreen;
|
||||
Reference in New Issue
Block a user