chore: add change password

This commit is contained in:
2026-03-01 16:32:43 +01:00
parent 7b23f82355
commit e4020c6388
8 changed files with 261 additions and 7 deletions
+27 -2
View File
@@ -11,10 +11,12 @@ import { ThemeProvider, useTheme } from "./src/context/ThemeContext";
import HomeScreen from "./src/screens/HomeScreen"; import HomeScreen from "./src/screens/HomeScreen";
import ClientLoginScreen from "./src/screens/auth/ClientLoginScreen"; import ClientLoginScreen from "./src/screens/auth/ClientLoginScreen";
import ChangePasswordScreen from "./src/screens/auth/ChangePasswordScreen";
import ClientNavigator from "./src/navigation/ClientNavigator"; import ClientNavigator from "./src/navigation/ClientNavigator";
import type { RootStackParamList } from "./src/navigation/types"; import type { RootStackParamList, ChangePasswordStackParamList } from "./src/navigation/types";
const Stack = createNativeStackNavigator<RootStackParamList>(); const Stack = createNativeStackNavigator<RootStackParamList>();
const CPStack = createNativeStackNavigator<ChangePasswordStackParamList>();
function AuthStack() { function AuthStack() {
const { colors } = useTheme(); const { colors } = useTheme();
@@ -40,8 +42,27 @@ function AuthStack() {
); );
} }
function ForceChangePasswordStack() {
const { colors } = useTheme();
return (
<CPStack.Navigator
screenOptions={{
headerStyle: { backgroundColor: colors.bgSecondary },
headerTintColor: colors.textWhite,
gestureEnabled: false,
}}
>
<CPStack.Screen
name="ChangePassword"
component={ChangePasswordScreen}
options={{ title: "Changer le mot de passe", headerLeft: () => null }}
/>
</CPStack.Navigator>
);
}
function RootNavigator() { function RootNavigator() {
const { isAuthenticated, isLoading, role } = useAuth(); const { isAuthenticated, isLoading, role, mustChangePassword } = useAuth();
const { colors, isDark } = useTheme(); const { colors, isDark } = useTheme();
if (isLoading) { if (isLoading) {
@@ -59,6 +80,10 @@ function RootNavigator() {
); );
} }
if (isAuthenticated && role === "client" && mustChangePassword) {
return <ForceChangePasswordStack />;
}
if (isAuthenticated && role === "client") { if (isAuthenticated && role === "client") {
return ( return (
<CartProvider> <CartProvider>
+1 -1
View File
@@ -40,7 +40,7 @@
"expo-notifications", "expo-notifications",
{ {
"icon": "./assets/icon.png", "icon": "./assets/icon.png",
"color": "#7C3AED", "color": "#000000",
"defaultChannel": "orders", "defaultChannel": "orders",
"sounds": [] "sounds": []
} }
+20
View File
@@ -507,6 +507,26 @@ export const cancelCommand = async (
// PENALTIES // PENALTIES
// ============================================ // ============================================
export const changePassword = async (
currentPassword: string,
newPassword: string,
): Promise<{ success: boolean; message?: string }> => {
try {
await apiClient.put(`${V1}/auth/change-password`, {
current_password: currentPassword,
new_password: newPassword,
});
return { success: true };
} catch (error: any) {
return {
success: false,
message:
error.response?.data?.error ||
"Erreur lors du changement de mot de passe",
};
}
};
export const getMyPenalties = async (): Promise<PenaltiesResponse> => { export const getMyPenalties = async (): Promise<PenaltiesResponse> => {
try { try {
const { data } = await apiClient.get(`${V1}/penalties`); const { data } = await apiClient.get(`${V1}/penalties`);
+1
View File
@@ -37,6 +37,7 @@ export interface UserResponse {
point?: number; point?: number;
point_zipette?: number; // ✅ AJOUTER CETTE LIGNE point_zipette?: number; // ✅ AJOUTER CETTE LIGNE
amende?: number; amende?: number;
must_change_password?: boolean;
} }
/** /**
+16 -3
View File
@@ -17,12 +17,14 @@ interface AuthState {
role: UserRole; role: UserRole;
isLoading: boolean; isLoading: boolean;
isAuthenticated: boolean; isAuthenticated: boolean;
mustChangePassword: boolean;
} }
interface AuthContextType extends AuthState { interface AuthContextType extends AuthState {
loginClient: (token: string) => Promise<void>; loginClient: (token: string, mustChangePassword?: boolean) => Promise<void>;
loginAdmin: (token: string, role: 'admin' | 'cabine' | 'livreur') => Promise<void>; loginAdmin: (token: string, role: 'admin' | 'cabine' | 'livreur') => Promise<void>;
logout: () => Promise<void>; logout: () => Promise<void>;
setMustChangePassword: (value: boolean) => void;
} }
const AuthContext = createContext<AuthContextType | undefined>(undefined); const AuthContext = createContext<AuthContextType | undefined>(undefined);
@@ -34,6 +36,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
role: null, role: null,
isLoading: true, isLoading: true,
isAuthenticated: false, isAuthenticated: false,
mustChangePassword: false,
}); });
// Restore session on mount // Restore session on mount
@@ -52,6 +55,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
role: 'client', role: 'client',
isLoading: false, isLoading: false,
isAuthenticated: true, isAuthenticated: true,
mustChangePassword: false,
}); });
return; return;
} }
@@ -65,6 +69,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
role: savedRole as UserRole, role: savedRole as UserRole,
isLoading: false, isLoading: false,
isAuthenticated: true, isAuthenticated: true,
mustChangePassword: false,
}); });
return; return;
} }
@@ -78,6 +83,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
role: null, role: null,
isLoading: false, isLoading: false,
isAuthenticated: false, isAuthenticated: false,
mustChangePassword: false,
}); });
} catch { } catch {
await clearAllAuth(); await clearAllAuth();
@@ -87,6 +93,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
role: null, role: null,
isLoading: false, isLoading: false,
isAuthenticated: false, isAuthenticated: false,
mustChangePassword: false,
}); });
} }
}; };
@@ -94,7 +101,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
restore(); restore();
}, []); }, []);
const loginClient = async (token: string) => { const loginClient = async (token: string, mustChangePassword = false) => {
const uname = extractUsernameFromToken(token); const uname = extractUsernameFromToken(token);
await storeToken(token); await storeToken(token);
if (uname) await setUsername(uname); if (uname) await setUsername(uname);
@@ -105,9 +112,14 @@ export function AuthProvider({ children }: { children: ReactNode }) {
role: 'client', role: 'client',
isLoading: false, isLoading: false,
isAuthenticated: true, isAuthenticated: true,
mustChangePassword,
}); });
}; };
const setMustChangePassword = (value: boolean) => {
setState(prev => ({ ...prev, mustChangePassword: value }));
};
const loginAdmin = async (token: string, role: 'admin' | 'cabine' | 'livreur') => { const loginAdmin = async (token: string, role: 'admin' | 'cabine' | 'livreur') => {
const uname = extractUsernameFromToken(token); const uname = extractUsernameFromToken(token);
await storeAdminToken(token); await storeAdminToken(token);
@@ -130,11 +142,12 @@ export function AuthProvider({ children }: { children: ReactNode }) {
role: null, role: null,
isLoading: false, isLoading: false,
isAuthenticated: false, isAuthenticated: false,
mustChangePassword: false,
}); });
}; };
return ( return (
<AuthContext.Provider value={{ ...state, loginClient, loginAdmin, logout }}> <AuthContext.Provider value={{ ...state, loginClient, loginAdmin, logout, setMustChangePassword }}>
{children} {children}
</AuthContext.Provider> </AuthContext.Provider>
); );
+4
View File
@@ -5,6 +5,10 @@ export type RootStackParamList = {
role: undefined; role: undefined;
}; };
export type ChangePasswordStackParamList = {
ChangePassword: undefined;
};
export type AuthStackParamList = { export type AuthStackParamList = {
RoleSelect: undefined; RoleSelect: undefined;
ClientLogin: undefined; ClientLogin: undefined;
@@ -0,0 +1,191 @@
import React, { useState } from "react";
import {
View,
Text,
TextInput,
TouchableOpacity,
StyleSheet,
Alert,
ActivityIndicator,
} from "react-native";
import { Feather } from "@expo/vector-icons";
import { useAuth } from "../../auth/AuthContext";
import { useTheme } from "../../context/ThemeContext";
import { changePassword } from "../../api/api";
export default function ChangePasswordScreen() {
const { setMustChangePassword } = useAuth();
const { colors } = useTheme();
const [currentPassword, setCurrentPassword] = useState("");
const [newPassword, setNewPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [showCurrent, setShowCurrent] = useState(false);
const [showNew, setShowNew] = useState(false);
const [showConfirm, setShowConfirm] = useState(false);
const [loading, setLoading] = useState(false);
const handleChangePassword = async () => {
if (!currentPassword || !newPassword || !confirmPassword) {
Alert.alert("Erreur", "Veuillez remplir tous les champs");
return;
}
if (newPassword.length < 8) {
Alert.alert(
"Erreur",
"Le nouveau mot de passe doit contenir au moins 8 caractères",
);
return;
}
if (newPassword !== confirmPassword) {
Alert.alert("Erreur", "Les nouveaux mots de passe ne correspondent pas");
return;
}
setLoading(true);
try {
const result = await changePassword(currentPassword, newPassword);
if (result.success) {
setMustChangePassword(false);
} else {
Alert.alert("Erreur", result.message || "Erreur inattendue");
}
} finally {
setLoading(false);
}
};
return (
<View style={[styles.container, { backgroundColor: colors.bgPrimary }]}>
<View style={styles.iconWrapper}>
<Feather name="lock" size={48} color="#7c3aed" />
</View>
<Text style={[styles.title, { color: colors.textPrimary }]}>
Changez votre mot de passe
</Text>
<Text style={[styles.subtitle, { color: colors.textSecondary }]}>
Pour accéder à l'application, vous devez définir un nouveau mot de
passe personnel.
</Text>
<View style={[styles.inputWrapper, { backgroundColor: colors.bgSecondary, borderColor: colors.border }]}>
<Feather name="lock" size={18} color={colors.textMuted} style={styles.inputIcon} />
<TextInput
style={[styles.input, { color: colors.textPrimary }]}
placeholder="Mot de passe actuel"
placeholderTextColor={colors.textMuted}
secureTextEntry={!showCurrent}
value={currentPassword}
onChangeText={setCurrentPassword}
autoCapitalize="none"
autoCorrect={false}
/>
<TouchableOpacity onPress={() => setShowCurrent(!showCurrent)}>
<Feather name={showCurrent ? "eye-off" : "eye"} size={18} color={colors.textMuted} />
</TouchableOpacity>
</View>
<View style={[styles.inputWrapper, { backgroundColor: colors.bgSecondary, borderColor: colors.border }]}>
<Feather name="key" size={18} color={colors.textMuted} style={styles.inputIcon} />
<TextInput
style={[styles.input, { color: colors.textPrimary }]}
placeholder="Nouveau mot de passe (min. 8 car.)"
placeholderTextColor={colors.textMuted}
secureTextEntry={!showNew}
value={newPassword}
onChangeText={setNewPassword}
autoCapitalize="none"
autoCorrect={false}
/>
<TouchableOpacity onPress={() => setShowNew(!showNew)}>
<Feather name={showNew ? "eye-off" : "eye"} size={18} color={colors.textMuted} />
</TouchableOpacity>
</View>
<View style={[styles.inputWrapper, { backgroundColor: colors.bgSecondary, borderColor: colors.border }]}>
<Feather name="check-circle" size={18} color={colors.textMuted} style={styles.inputIcon} />
<TextInput
style={[styles.input, { color: colors.textPrimary }]}
placeholder="Confirmer le nouveau mot de passe"
placeholderTextColor={colors.textMuted}
secureTextEntry={!showConfirm}
value={confirmPassword}
onChangeText={setConfirmPassword}
autoCapitalize="none"
autoCorrect={false}
/>
<TouchableOpacity onPress={() => setShowConfirm(!showConfirm)}>
<Feather name={showConfirm ? "eye-off" : "eye"} size={18} color={colors.textMuted} />
</TouchableOpacity>
</View>
<TouchableOpacity
style={[styles.button, loading && styles.buttonDisabled]}
onPress={handleChangePassword}
disabled={loading}
>
{loading ? (
<ActivityIndicator color="#fff" />
) : (
<Text style={styles.buttonText}>Confirmer</Text>
)}
</TouchableOpacity>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 24,
justifyContent: "center",
},
iconWrapper: {
alignItems: "center",
marginBottom: 16,
},
title: {
fontSize: 22,
fontWeight: "bold",
marginBottom: 10,
textAlign: "center",
},
subtitle: {
fontSize: 14,
marginBottom: 32,
textAlign: "center",
lineHeight: 20,
},
inputWrapper: {
flexDirection: "row",
alignItems: "center",
borderWidth: 1,
borderRadius: 8,
paddingHorizontal: 12,
height: 50,
marginBottom: 14,
},
inputIcon: {
marginRight: 8,
},
input: {
flex: 1,
fontSize: 15,
},
button: {
backgroundColor: "#7c3aed",
height: 50,
borderRadius: 8,
justifyContent: "center",
alignItems: "center",
marginTop: 8,
},
buttonDisabled: {
opacity: 0.7,
},
buttonText: {
color: "#fff",
fontSize: 16,
fontWeight: "600",
},
});
@@ -60,7 +60,7 @@ const LoginClient = () => {
formData.password, formData.password,
); );
if (result.success && result.access_token) { if (result.success && result.access_token) {
await loginClient(result.access_token); await loginClient(result.access_token, result.user?.must_change_password ?? false);
} else { } else {
const errorMessage = const errorMessage =
result.message || "Identifiants incorrects"; result.message || "Identifiants incorrects";