From 5b61e69dd31c8eabd5fb61050cba55fae5c7d888 Mon Sep 17 00:00:00 2001 From: genjiguru <54900323+jasmineguru@users.noreply.github.com> Date: Sun, 10 Dec 2023 18:01:43 -0500 Subject: [PATCH] token issues what's happening: - in the text file you input what emial you want to udpate - after reauthing, a confirmation email is sent to the new email, you click and the link and oyu get an email in ur old email confirming the changes (no need to do anything for this email) - when you look at the firebase console, you see the changes have been made - the issue is in backend logs (not necessarily the stuf mentioned in frontend logs) - when user navigates out of updateprofile and come back to it, then the backend logs show errors bc of revoked token and then frontend discornnexts backend. - as one could see i tried fooling around with backend code if that'd do anything --- backend/routes/users.py | 15 ++ backend/utils/FirebaseAPI.py | 8 + frontend/assets/colorConstants.tsx | 6 +- frontend/package.json | 2 +- frontend/src/APIs/FLASK_API.tsx | 2 +- frontend/src/APIs/UsersAPI.ts | 32 +++- frontend/src/components/appNavigation.tsx | 35 ++-- frontend/src/screens/settings.tsx | 2 +- .../src/screens/settings/updateProfile.tsx | 177 ++++++++++++++++-- frontend/src/utilities/firebase.ts | 15 +- 10 files changed, 253 insertions(+), 41 deletions(-) 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; + } + + } };