chore: add change password
This commit is contained in:
+27
-2
@@ -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
@@ -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": []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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`);
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
Reference in New Issue
Block a user