diff --git a/backend/routes/users.py b/backend/routes/users.py index 98635df..fc8f809 100644 --- a/backend/routes/users.py +++ b/backend/routes/users.py @@ -82,4 +82,19 @@ def update_user(user_id: str) -> Response: except CarbonTrackError as e: abort(code=400, description=f"{e}") +@users.route("/user/update_email/", methods=["PATCH"]) +@carbon_auth.auth.login_required +def update_user_email(user_id: str) -> Response: + try: + query = {"_id": ObjectId(user_id)} + new_email = request.get_json().get('email', '') + current_user = carbon_auth.auth.current_user() + new_token = FirebaseAPI.refresh_token(current_user.uid) + CarbonTrackDB.users_coll.update_one(query, {'$set': {'email': new_email}}) + item = CarbonTrackDB.users_coll.find_one(query) + item = User.from_json(item).to_json() + return jsonify({'user': item}) + except CarbonTrackError as e: + abort(code=400, description=f"{e}") + diff --git a/backend/utils/FirebaseAPI.py b/backend/utils/FirebaseAPI.py index 2d18964..d611ae0 100644 --- a/backend/utils/FirebaseAPI.py +++ b/backend/utils/FirebaseAPI.py @@ -26,3 +26,11 @@ def get_user(id_token: str) -> Optional[User]: item = CarbonTrackDB.users_coll.find_one(query) item = User.from_json(item) return item + @staticmethod + def refresh_token(user_id): + try: + user = auth.get_user(user_id) + new_id_token = user.refresh_id_tokens() + return new_id_token + except auth.AuthError as e: + raise e \ No newline at end of file diff --git a/frontend/assets/colorConstants.tsx b/frontend/assets/colorConstants.tsx index ee0f0b7..e264be5 100644 --- a/frontend/assets/colorConstants.tsx +++ b/frontend/assets/colorConstants.tsx @@ -1,6 +1,6 @@ const Colors = { LIGHTFGREEN: '#E0EEC6', - DARKGREEN: '#243E36', + DARKGREEN: '#10302b', DARKGREEN2: '#224A3E', DARKGREEN3: '#2E5C4E', WHITE: '#fff', @@ -14,7 +14,9 @@ const Colors = { BLUE: 'blue', TEAL: '#6bcfca', TRANSLIGHTGREEN: '#c6dbb2', - TRANSGREENBACK: '#07332b' + TRANSLIGHTGREEN2: '#b3c99d', + TRANSGREENBACK: '#07332b', + DARKDARKGREEN: '#031c19', }; export default Colors; \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index 7401a5a..93dcfaf 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -31,7 +31,7 @@ "expo-font": "~11.4.0", "expo-image-picker": "~14.3.2", "expo-status-bar": "~1.6.0", - "firebase": "^10.5.2", + "firebase": "^10.7.1", "formik": "^2.4.5", "mongodb": "^6.3.0", "react": "^18.2.0", diff --git a/frontend/src/APIs/FLASK_API.tsx b/frontend/src/APIs/FLASK_API.tsx index e8e0260..7327b7b 100644 --- a/frontend/src/APIs/FLASK_API.tsx +++ b/frontend/src/APIs/FLASK_API.tsx @@ -1,6 +1,6 @@ import axios from "axios"; import firebaseService from "../utilities/firebase"; -const FLASK_LOCAL_ADDRESS: string = "http://100.112.45.130:6050"; +const FLASK_LOCAL_ADDRESS: string = "http://192.168.2.12:6050"; // Function to get the Firebase authentication token const getFirebaseAuthToken = async (): Promise => { diff --git a/frontend/src/APIs/UsersAPI.ts b/frontend/src/APIs/UsersAPI.ts index 89f52a6..7c4c169 100644 --- a/frontend/src/APIs/UsersAPI.ts +++ b/frontend/src/APIs/UsersAPI.ts @@ -67,5 +67,35 @@ export const UsersAPI = { console.error('Temp tip: have you started the backend?: '); return undefined; } - } + }, + updateUserEmail: async (userId: ObjectId, newEmail: string) => { + try { + // Ensure that firebaseUser is defined before using it + const firebaseUser = await firebaseService.getFirebaseUser(); + if (firebaseUser == null) { + // Handle the case when the user is not signed in + console.error('Firebase user is undefined'); + return undefined; + } + + // Get the refreshed token + const newToken = await firebaseUser.getIdToken(true); + + // Make the request to update the email with the new token + const res = await FLASK_HTTPS.patch( + routeName + `/user/update_email/${userId.toHexString()}`, + { email: newEmail }, + { + headers: { + Authorization: `Bearer ${newToken}`, + }, + } + ); + + return res.data.user as User; + } catch (error) { + console.error('UsersAPI(frontend): updateUserEmailError:', error); + return undefined; + } + }, } diff --git a/frontend/src/components/appNavigation.tsx b/frontend/src/components/appNavigation.tsx index 7134beb..f58beb0 100644 --- a/frontend/src/components/appNavigation.tsx +++ b/frontend/src/components/appNavigation.tsx @@ -156,23 +156,6 @@ const MainAppTabs = (): JSX.Element => { ), }} /> - ( - - ), - }} - /> { /> ), }} + /> + ( + + ), + }} /> + ); }; diff --git a/frontend/src/screens/settings.tsx b/frontend/src/screens/settings.tsx index 5437f24..21e200c 100644 --- a/frontend/src/screens/settings.tsx +++ b/frontend/src/screens/settings.tsx @@ -50,7 +50,7 @@ const styles = StyleSheet.create({ width: '85%', height: 56, borderRadius: 15, - backgroundColor: Colors.TRANSLIGHTGREEN, + backgroundColor: Colors.TRANSLIGHTGREEN2, marginVertical: 10, justifyContent: 'center', }, diff --git a/frontend/src/screens/settings/updateProfile.tsx b/frontend/src/screens/settings/updateProfile.tsx index c28a285..0b00e47 100644 --- a/frontend/src/screens/settings/updateProfile.tsx +++ b/frontend/src/screens/settings/updateProfile.tsx @@ -1,14 +1,16 @@ import React, { useState, useEffect } from 'react'; -import { ScrollView, View, Text, StyleSheet, TouchableOpacity, Image } from 'react-native'; +import { ScrollView, View, Text, StyleSheet, TouchableOpacity, Image, TextInput, Alert, Modal } from 'react-native'; import Colors from '../../../assets/colorConstants'; -import type { RootStackParamList } from '../../components/types'; +import {type RootStackParamList} from '../../components/types'; import type { StackNavigationProp } from '@react-navigation/stack'; import { useNavigation } from '@react-navigation/native'; import Ionicons from '@expo/vector-icons/Ionicons'; import { useFonts } from 'expo-font'; import firebaseService from '../../utilities/firebase'; import { launchImageLibraryAsync, type ImagePickerResult, MediaTypeOptions, } from 'expo-image-picker'; - +import { type User } from '../../models/User'; +import { UsersAPI } from '../../APIs/UsersAPI'; +import { EmailAuthProvider, reauthenticateWithCredential, verifyBeforeUpdateEmail, onAuthStateChanged } from 'firebase/auth'; export type StackNavigation = StackNavigationProp; export default function UpdateProfileScreen(): JSX.Element { @@ -20,16 +22,113 @@ export default function UpdateProfileScreen(): JSX.Element { const [userid, setUserid] = useState(''); const [photoURL, setPhotoURL] = useState(null); const [rerenderKey, setRerenderKey] = useState(0); + const [loggedUser, setLoggedUser] = useState(undefined); + const [newEmail, setNewEmail] = useState(''); + const [newName, setNewName] = useState(''); + useEffect(() => { const fetchUserData = async (): Promise => { const user = await firebaseService.getFirebaseUser(); setUserid(user?.uid ?? ''); - }; + const userPhotoURL = user?.photoURL ?? null ; + setPhotoURL(userPhotoURL); + void UsersAPI.GetLoggedInUser().then((res) => { + if (res != null) { + setLoggedUser(res); + } + }); + }; void fetchUserData(); }, [rerenderKey]); + + const handleUpdateEmail = async (): Promise => { + try { + const user = await firebaseService.getFirebaseUser(); + + if (user != null) { + const userCreds = await promptUserForCredentials(); + + if (userCreds != null && user.email != null) { + const entireCreds = EmailAuthProvider.credential(user.email, userCreds.password); + await reauthenticateWithCredential(user, entireCreds); + console.log(newEmail); + await verifyBeforeUpdateEmail(user, newEmail); + await waitForEmailVerification(user); + await firebaseService.updateUserEmail(user, newEmail); + + // this i ask gpt + // Refresh the ID token after email update + await user.getIdToken(/* forceRefresh */ true); + // Update the email on the backend (MongoDB) + if (loggedUser != null) { + // Update the user in MongoDB + await UsersAPI.updateUserEmail(loggedUser._id, newEmail); + } + } + } + } catch (error: any) { + // Handle errors... + console.error('Error updating email:', error); + + if (error.code === 'auth/id-token-revoked') { + // Handle token revocation gracefully + Alert.alert('Your session has expired. Please sign in again.'); + await firebaseService.signOutUser(); // Sign out locally + // Navigate to the login or authentication screen + // Update UI to reflect that the user is not authenticated + navigation.navigate('LogIn'); + } else { + Alert.alert('An unexpected error occurred. Please try again.'); + throw error; + } + } + }; + + + + + + + + + const waitForEmailVerification = async (user: any): Promise => { + return await new Promise((resolve, reject) => { + const unsubscribe = onAuthStateChanged(user, (updatedUser) => { + if ((updatedUser?.emailVerified) ?? false) { + unsubscribe(); + resolve(); + } + }, reject); + }); + }; + + + + + const promptUserForCredentials = async (): Promise<{ password: string } | null> => { + return await new Promise((resolve) => { + Alert.prompt( + 'Reauthentication', + 'Please enter your current password:', + [ + { + text: 'Cancel', + onPress: () => resolve(null), + style: 'cancel', + }, + { + text: 'Submit', + onPress: (password) => resolve({ password: password ?? '' }), + }, + ], + 'secure-text' + ); + }); + }; + const handleProfilePictureUpload = async (): Promise => { try { const result: ImagePickerResult = await launchImageLibraryAsync({ @@ -52,8 +151,6 @@ export default function UpdateProfileScreen(): JSX.Element { return <>; } - - return ( @@ -71,28 +168,43 @@ export default function UpdateProfileScreen(): JSX.Element { Edit Photo - - - - - - + + Name + setNewName(text)} + /> + + Email + setNewEmail(text)} + /> + + + Update Profile + + ); } const styles = StyleSheet.create({ container: { flexGrow: 1, - backgroundColor: Colors.DARKGREEN, + backgroundColor: Colors.DARKDARKGREEN, }, profileContainer:{ height: 645, width: '85%', - backgroundColor: Colors.TRANSGREENBACK, + backgroundColor: Colors.DARKGREEN, borderRadius: 20, alignSelf: 'center', margin: 40 @@ -149,9 +261,42 @@ const styles = StyleSheet.create({ textAlign: 'center', top: 15, }, - infoContainer:{ - padding: 5, + textInputBox:{ + alignSelf: 'center', + top: '10%', + padding: 10, + }, + label:{ + fontSize: 16, + opacity: 0.5, + color: Colors.WHITE + }, + textInput: { + paddingHorizontal: 5, + marginBottom: 30, + marginTop: 10, + borderRadius: 10, + fontSize: 16, + borderBottomWidth: 1, + borderColor: Colors.WHITE, + color: Colors.WHITE, + width: 270 + }, + saveButton: { + borderRadius: 5, + alignSelf: 'center', + top: '20%', + }, + saveButtonText:{ + color: Colors.LIGHTFGREEN, + fontSize: 16, + textDecorationLine: 'underline', + fontWeight: '400', + shadowColor: Colors.LIGHTFGREEN, + + } + }); \ No newline at end of file diff --git a/frontend/src/utilities/firebase.ts b/frontend/src/utilities/firebase.ts index 92daea5..2b90618 100644 --- a/frontend/src/utilities/firebase.ts +++ b/frontend/src/utilities/firebase.ts @@ -6,7 +6,9 @@ import { signOut, initializeAuth, getReactNativePersistence, - updateProfile + updateProfile, + updateEmail, + type User, } from 'firebase/auth'; import ReactNativeAsyncStorage from '@react-native-async-storage/async-storage'; import { getStorage, ref as storageRef, uploadBytes, getDownloadURL } from "firebase/storage"; @@ -49,7 +51,6 @@ const firebaseService = { signOutUser: async () => { await signOut(auth); }, - uploadProfilePicture: async (userId: string, imageUri: string): Promise => { const storagePath = `profilePictures/${userId}/profilePicture.jpg`; const profilePictureRef = storageRef(storage, storagePath); @@ -79,6 +80,16 @@ const firebaseService = { return null; }, + updateUserEmail: async(user: User, newEmail: string) =>{ + try { + await updateEmail(user, newEmail); + console.log('Firebase (frontend): Email updated successfully in Firebase'); + } catch (error) { + console.error('Firebase(frontend: Error updating email in Firebase:', error); + throw error; + } + + } };