-1

This is a follow up to this question. I have a firebase function which is supposed to take an OTP, validate it and then change a user's password based on whether it is correct or not (for some reason I'm not able to use firebase's inbuild password reset functionality). Following is my function:

exports.resetPassword = functions.https.onCall((data, context) => {
    return new Promise((resolve, reject) => {
        if(data.sesId && data.otp){
            admin.firestore().collection('verification').doc(data.sesId).get().then(verSnp => {
                if(verSnp.data().attempt != 'verified'){
                    var now = new Date().getTime()
                    if(verSnp.data().expiring > now){
                        if(data.email == verSnp.data().email){
                            if(verSnp.data().attempt > 0){
                                if(data.otp == verSnp.data().otp){
                                    admin.auth().getUserByEmail(data.email).then(user => {
                                        admin.auth().updateUser(user.uid,{
                                            password: data.password
                                        }).then(() => {
                                            admin.firestore().collection('verification').doc(data.sesId).update({
                                                attempt: 'verified'
                                            }).then(() => {
                                                Promise.resolve()
                                            }).catch(() => {
                                                throw new Error('Error updating the database.')
                                            })
                                        }).catch(() => {
                                            throw new Error('Error updating the password. Please try again.')
                                        })
                                    }).catch(() => {
                                        throw new Error('Incorrect email. How did you get here?')
                                    })
                                } else {
                                    var redAttempt = verSnp.data().attempt - 1
                                    admin.firestore().collection('verification').doc(data.sesId).update({
                                        attempt: redAttempt
                                    }).then(() => {
                                        throw new Error(`Incorrect OTP. You have ${redAttempt} attempts remaining.`)
                                    }).catch(() => {
                                        throw new Error('Wrong OTP, try again.')
                                    })
                                }
                            } else {
                                throw new Error('Incorrect OTP. You have exhausted your attempts. Please request a new OTP.')
                            }
                        } else {
                            throw new Error('Incorrect email. How did you get here?')
                        }
                    } else {
                        throw new Error('OTP is expired. Please request a new OTP.')
                    }
                } else {
                    throw new Error('OTP is invalid. Please request a new OTP.')
                }
            }).catch(() => {
                throw new Error('Invalid session id. Please request the OTP through Forgot Password.')
            })
        } else {
            throw new Error('Enter OTP')
        }
    })
})

When I run the function, it gets executed, because I can see it in the console statements, but I'm getting following error on my client side.

Access to fetch at 'https://us-central1-project-name.cloudfunctions.net/functionName' from origin 'http://localhost:8080' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

and when I log the response received from the function, it shows {"code":"internal"}.

What is the cors package? How do I solve this problem?


Part 2 (not related)

Also, on lines 11 and 12 of my function, I'm using

admin.auth().getUserByEmail(data.email).then(user => {
  admin.auth().updateUser(user.uid, {password: data.password})
})

Is this correct?


For part 1 I referred to this question, but it has no answers.

VaibhavJoshi
  • 355
  • 2
  • 15

1 Answers1

2

Have a look at the documentation for Callable Cloud Functions:

  1. You don't need to encapsulate it in return new Promise((resolve, reject) => {});
  2. You need to return data that can be JSON encoded;
  3. You need to manage the errors correctly, by by throwing (or returning a Promise rejected with) an instance of functions.https.HttpsError;
  4. You need to correctly chain all the promises returned by the asynchronous methods.

I've tried below to re-organized your code in the lights of the points above, but since your business logic is complex I cannot test it and there might be other approaches to manage all the cases... Up to you to "polish" this first attempt! Hoping it will help.

exports.resetPassword = functions.https.onCall((data, context) => {

        if(data.sesId && data.otp){

            let dataOptCorresponds = true;

            return admin.firestore().collection('verification').doc(data.sesId).get()
            .then(verSnp => {
                if(verSnp.data().attempt != 'verified'){

                    var now = new Date().getTime()

                    if(verSnp.data().expiring > now){
                        if(data.email == verSnp.data().email){
                            if(verSnp.data().attempt > 0){
                                if(data.otp == verSnp.data().otp){
                                    return admin.auth().getUserByEmail(data.email);
                                } else {
                                    dataOptCorresponds = false;
                                    var redAttempt = verSnp.data().attempt - 1
                                    return admin.firestore().collection('verification').doc(data.sesId).update({
                                        attempt: redAttempt
                                    })
                                }
                            } else {
                                throw new Error('Incorrect OTP. You have exhausted your attempts. Please request a new OTP.')
                            }
                        } else {
                            throw new Error('Incorrect email. How did you get here?')
                        }
                    } else {
                        throw new Error('OTP is expired. Please request a new OTP.')
                    }
                } else {
                    throw new Error('OTP is invalid. Please request a new OTP.')
                }
            })
            .then(user => {
                if(dataOptCorresponds) {
                    return admin.auth().updateUser(user.uid,{
                        password: data.password
                    })
                } else {
                    throw new Error(`Incorrect OTP. You have xxxx attempts remaining.`)
                }
            })
            .then(() => {
                return admin.firestore().collection('verification').doc(data.sesId).update({
                    attempt: 'verified'
                })
            .then(() => {
                return {result: "success"}                      
            })          
            .catch(error => {
                throw new functions.https.HttpsError('internal', error.message);

            })

        } else {

            throw new functions.https.HttpsError('invalid-argument', 'Enter OTP');
        }

})

UPDATE following Bergi's comment below:

If you want to be able to differentiate the kind of errors returned to the front-end (in particular sending back an invalid-argument HttpsError if the OTP is incorrect, invalid or expired or if the email is incorrect) you may use a second argument in the then() method.

exports.resetPassword = functions.https.onCall((data, context) => {

        if(data.sesId && data.otp){

            let dataOptCorresponds = true;

            return admin.firestore().collection('verification').doc(data.sesId).get()
            .then(

                verSnp => {
                    if(verSnp.data().attempt != 'verified'){

                        var now = new Date().getTime()

                        if(verSnp.data().expiring > now){
                            if(data.email == verSnp.data().email){
                                if(verSnp.data().attempt > 0){
                                    if(data.otp == verSnp.data().otp){
                                        return admin.auth().getUserByEmail(data.email);
                                    } else {
                                        dataOptCorresponds = false;
                                        var redAttempt = verSnp.data().attempt - 1
                                        return admin.firestore().collection('verification').doc(data.sesId).update({
                                            attempt: redAttempt
                                        })
                                    }
                                } else {
                                    throw new Error('Incorrect OTP. You have exhausted your attempts. Please request a new OTP.')
                                }
                            } else {
                                throw new Error('Incorrect email. How did you get here?')
                            }
                        } else {
                            throw new Error('OTP is expired. Please request a new OTP.')
                        }
                    } else {
                        throw new Error('OTP is invalid. Please request a new OTP.')
                    }
                },

                error => {

                    throw new functions.https.HttpsError('invalid-argument', error.message);

                }

            )
            .then(user => {
                if(dataOptCorresponds) {
                    return admin.auth().updateUser(user.uid,{
                        password: data.password
                    })
                } else {
                    throw new Error(`Incorrect OTP. You have xxxx attempts remaining.`)
                }
            })
            .then(() => {
                return admin.firestore().collection('verification').doc(data.sesId).update({
                    attempt: 'verified'
                })
            .then(() => {
                return {result: "success"}                      
            })          
            .catch(error => {
                throw new functions.https.HttpsError('internal', error.message);

            })

        } else {

            throw new functions.https.HttpsError('invalid-argument', 'Enter OTP');
        }

})
Renaud Tarnec
  • 79,263
  • 10
  • 95
  • 121
  • With the `.catch()` in the end, all of the `throw`n errors end up as an internal `HttpsError`. You'll need to move that up the chain or [use the second `then` parameter](https://stackoverflow.com/a/24663315/1048572). – Bergi Dec 18 '19 at 14:16
  • @Bergi Thanks for the "tip"! I've tried to adapt the answer. It would be great if you could have a look and confirm it is the correct approach! I don't see, however, how to implement a second `then()` parameter where we have `if(dataOptCorresponds)` since the error is function of the `dataOptCorresponds` value... – Renaud Tarnec Dec 18 '19 at 15:27
  • 1
    Ooops, forget what I was saying. I had not properly read your answer and didn't realise that you actually *wanted* all of the error messages to get wrapped in the `HttpsError`. I guess your original approach is the correct one. – Bergi Dec 18 '19 at 20:17
  • 1
    Guys I really appreciate the help, but I'm not at my desk for a week due to holidays. I will respond as soon as I get the chance to try out the code. – VaibhavJoshi Dec 23 '19 at 07:42
  • Okay, this works, but now I have another method, where I want to update multiple documents in 3 different collections. Will chaining 3 promises work? Eg.: return admin.firestore().collection('collection-1').doc(docid).update({...}) .then(() => {admin.firestore().collection('collection-2').doc(docid-2).set({...})}) .then(() => {admin.firestore().collection('collection-3').doc(docid-3).delete()}) .catch(err => throw new Error(err.message)) – VaibhavJoshi Jan 07 '20 at 13:15
  • 1
    @VaibhavJoshi Why don't you use a [batched write](https://firebase.google.com/docs/firestore/manage-data/transactions?authuser=1#batched-writes) to update multiple documents in 3 different collections? – Renaud Tarnec Jan 07 '20 at 13:33