diff --git a/mobile/App.tsx b/mobile/App.tsx index 63bf56b9..69721f58 100644 --- a/mobile/App.tsx +++ b/mobile/App.tsx @@ -11,10 +11,12 @@ import { ThemeProvider, useTheme } from "./src/context/ThemeContext"; import HomeScreen from "./src/screens/HomeScreen"; import ClientLoginScreen from "./src/screens/auth/ClientLoginScreen"; +import ChangePasswordScreen from "./src/screens/auth/ChangePasswordScreen"; import ClientNavigator from "./src/navigation/ClientNavigator"; -import type { RootStackParamList } from "./src/navigation/types"; +import type { RootStackParamList, ChangePasswordStackParamList } from "./src/navigation/types"; const Stack = createNativeStackNavigator(); +const CPStack = createNativeStackNavigator(); function AuthStack() { const { colors } = useTheme(); @@ -40,8 +42,27 @@ function AuthStack() { ); } +function ForceChangePasswordStack() { + const { colors } = useTheme(); + return ( + + null }} + /> + + ); +} + function RootNavigator() { - const { isAuthenticated, isLoading, role } = useAuth(); + const { isAuthenticated, isLoading, role, mustChangePassword } = useAuth(); const { colors, isDark } = useTheme(); if (isLoading) { @@ -59,6 +80,10 @@ function RootNavigator() { ); } + if (isAuthenticated && role === "client" && mustChangePassword) { + return ; + } + if (isAuthenticated && role === "client") { return ( diff --git a/mobile/app.json b/mobile/app.json index 4457bc5b..6310e5c1 100644 --- a/mobile/app.json +++ b/mobile/app.json @@ -40,7 +40,7 @@ "expo-notifications", { "icon": "./assets/icon.png", - "color": "#7C3AED", + "color": "#000000", "defaultChannel": "orders", "sounds": [] } diff --git a/mobile/src/api/api.ts b/mobile/src/api/api.ts index 793a1393..9cd35af7 100644 --- a/mobile/src/api/api.ts +++ b/mobile/src/api/api.ts @@ -507,6 +507,26 @@ export const cancelCommand = async ( // 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 => { try { const { data } = await apiClient.get(`${V1}/penalties`); diff --git a/mobile/src/api/api_types.ts b/mobile/src/api/api_types.ts index 553a0c8d..847c3ebb 100644 --- a/mobile/src/api/api_types.ts +++ b/mobile/src/api/api_types.ts @@ -37,6 +37,7 @@ export interface UserResponse { point?: number; point_zipette?: number; // ✅ AJOUTER CETTE LIGNE amende?: number; + must_change_password?: boolean; } /** diff --git a/mobile/src/auth/AuthContext.tsx b/mobile/src/auth/AuthContext.tsx index c373c9ff..f2d38f13 100644 --- a/mobile/src/auth/AuthContext.tsx +++ b/mobile/src/auth/AuthContext.tsx @@ -17,12 +17,14 @@ interface AuthState { role: UserRole; isLoading: boolean; isAuthenticated: boolean; + mustChangePassword: boolean; } interface AuthContextType extends AuthState { - loginClient: (token: string) => Promise; + loginClient: (token: string, mustChangePassword?: boolean) => Promise; loginAdmin: (token: string, role: 'admin' | 'cabine' | 'livreur') => Promise; logout: () => Promise; + setMustChangePassword: (value: boolean) => void; } const AuthContext = createContext(undefined); @@ -34,6 +36,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { role: null, isLoading: true, isAuthenticated: false, + mustChangePassword: false, }); // Restore session on mount @@ -52,6 +55,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { role: 'client', isLoading: false, isAuthenticated: true, + mustChangePassword: false, }); return; } @@ -65,6 +69,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { role: savedRole as UserRole, isLoading: false, isAuthenticated: true, + mustChangePassword: false, }); return; } @@ -78,6 +83,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { role: null, isLoading: false, isAuthenticated: false, + mustChangePassword: false, }); } catch { await clearAllAuth(); @@ -87,6 +93,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { role: null, isLoading: false, isAuthenticated: false, + mustChangePassword: false, }); } }; @@ -94,7 +101,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { restore(); }, []); - const loginClient = async (token: string) => { + const loginClient = async (token: string, mustChangePassword = false) => { const uname = extractUsernameFromToken(token); await storeToken(token); if (uname) await setUsername(uname); @@ -105,9 +112,14 @@ export function AuthProvider({ children }: { children: ReactNode }) { role: 'client', isLoading: false, isAuthenticated: true, + mustChangePassword, }); }; + const setMustChangePassword = (value: boolean) => { + setState(prev => ({ ...prev, mustChangePassword: value })); + }; + const loginAdmin = async (token: string, role: 'admin' | 'cabine' | 'livreur') => { const uname = extractUsernameFromToken(token); await storeAdminToken(token); @@ -130,11 +142,12 @@ export function AuthProvider({ children }: { children: ReactNode }) { role: null, isLoading: false, isAuthenticated: false, + mustChangePassword: false, }); }; return ( - + {children} ); diff --git a/mobile/src/navigation/types.ts b/mobile/src/navigation/types.ts index a02c56fb..6635c2b6 100644 --- a/mobile/src/navigation/types.ts +++ b/mobile/src/navigation/types.ts @@ -5,6 +5,10 @@ export type RootStackParamList = { role: undefined; }; +export type ChangePasswordStackParamList = { + ChangePassword: undefined; +}; + export type AuthStackParamList = { RoleSelect: undefined; ClientLogin: undefined; diff --git a/mobile/src/screens/auth/ChangePasswordScreen.tsx b/mobile/src/screens/auth/ChangePasswordScreen.tsx new file mode 100644 index 00000000..0b6ab3aa --- /dev/null +++ b/mobile/src/screens/auth/ChangePasswordScreen.tsx @@ -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 ( + + + + + + Changez votre mot de passe + + + Pour accéder à l'application, vous devez définir un nouveau mot de + passe personnel. + + + + + + setShowCurrent(!showCurrent)}> + + + + + + + + setShowNew(!showNew)}> + + + + + + + + setShowConfirm(!showConfirm)}> + + + + + + {loading ? ( + + ) : ( + Confirmer + )} + + + ); +} + +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", + }, +}); diff --git a/mobile/src/screens/auth/ClientLoginScreen.tsx b/mobile/src/screens/auth/ClientLoginScreen.tsx index 2fb54092..ee7ce245 100644 --- a/mobile/src/screens/auth/ClientLoginScreen.tsx +++ b/mobile/src/screens/auth/ClientLoginScreen.tsx @@ -60,7 +60,7 @@ const LoginClient = () => { formData.password, ); if (result.success && result.access_token) { - await loginClient(result.access_token); + await loginClient(result.access_token, result.user?.must_change_password ?? false); } else { const errorMessage = result.message || "Identifiants incorrects";