0

I have implemented firebase auth in my app. I need to update the photo URL using a Cloud Function. So when the user uploads a photo to storage, I need to update the URL in the user object with the URL of the image which is uploaded at images/uid.png

I need to find the user by uid and then update the user object with the new URL but I cannot do that. This what I have tried:

exports.updateUrl = functions.auth.getUser(uid)(async (user) => {
  const newPhotoUrl = storage.bucket().file(`images/${uid}.jpg`);
  await admin.auth().updateUser(user.uid, {photoUrl: newPhotoUrl});

  return true;
});

I've found this answer but it contains only broken links :(

How to solve this?

Renaud Tarnec
  • 79,263
  • 10
  • 95
  • 121
Joan P.
  • 2,368
  • 6
  • 30
  • 63

1 Answers1

1

When the user uploads a photo to storage

This means that you need to use a Cloud Function that is triggered by a Cloud Storage event and not a CF triggered by the Authentication Service event (BTW functions.auth.getUser() does not exist, see the doc).

The following should do the trick (untested).

const functions = require("firebase-functions");

const admin = require("firebase-admin");
admin.initializeApp()

exports.updateUrl = functions.storage.object().onFinalize(async (object) => {

    try {

        const newPhotoStorageFile = admin.storage().bucket(object.bucket).file(object.name);
        const signedURLconfig = { action: 'read', expires: '01-01-2030' };
        const newPhotoURLArray = await newPhotoStorageFile.getSignedUrl(signedURLconfig);
        const newPhotoURL = newPhotoURLArray[0];
        
        const newPhotoStoragePath = object.name;
        const userId = newPhotoStoragePath.split("/")[1].split(".")[0]; // Double check this simple extraction code is valid for all your cases.

        await admin.auth().updateUser(userId, { photoUrl: newPhotoURL });

        return true;

    } catch (error) {
        console.log(error);
        return null;
    }

});

At this stage if you run this Cloud Function you'll get the following error: Error: Permission 'iam.serviceAccounts.signBlob' denied on resource (or it may not exist).

Assigning, via the Google Cloud console, this IAM permission to the App Engine default service account, PROJECT_ID @appspot.gserviceaccount.com would solve the problem.

HOWEVER, you would face another problem: The SignedURL has a short life duration due the problem described in this SO answer. So you need to use the Google Cloud Storage npm module and initialize it with a service account json file.

const {Storage} = require('@google-cloud/storage');
const storage = new Storage({keyFilename: "key.json"});

const newPhotoStorageFile = storage.bucket(object.bucket).file(object.name);
Renaud Tarnec
  • 79,263
  • 10
  • 95
  • 121
  • Thanks again Renaud. Where does this `newPhotoUrl` come from? – Joan P. May 12 '23 at 08:37
  • Hey! Just a typo!! It should be `newPhotoStoragePath`. With `object.name` we get the path of the file that triggered the Cloud Function. – Renaud Tarnec May 12 '23 at 08:39
  • Let me try and provide a feed-back. – Joan P. May 12 '23 at 09:05
  • I could successfully deploy the function, but I get an error `The photoURL field must be a valid URL. at validateCreateEditRequest (/workspace/node_modules/firebase-admin/lib/auth/auth-api-request.js:380:15) at /workspace/node_modules/firebase-admin/lib/auth/auth-api-request.js:619:5`. Why is that? I voted-up, thank you for helping again. – Joan P. May 12 '23 at 09:24
  • I think something is wrong with `photoUrl: newPhotoStoragePath` but I cannot see what. Do you? – Joan P. May 12 '23 at 09:33
  • I just tried to use instead of `photoUrl: newPhotoStoragePath`, `photoUrl: object.selfLink` but it fails with `"code": 401"`. I think because it doesn't contain a token. I lost. – Joan P. May 12 '23 at 09:44
  • There was an error in my code, I forgot to generate the URL from the file. As I said it was untested :-) . It is corrected. – Renaud Tarnec May 12 '23 at 09:48
  • Thanks for updating the answer. I really appreciate it. With the new code, I get `The action is not provided or invalid. at File.getSignedUrl (/workspace/node_modules/@google-cloud/storage/build/src/file.js:2184:19)`. – Joan P. May 12 '23 at 10:06
  • Any clues? I think that this is the last step before make it work correctly. – Joan P. May 12 '23 at 10:48
  • I just find another [answer](https://stackoverflow.com/questions/68918518/how-to-get-firebase-generated-download-url-on-firebase-function) of your, is it still available? – Joan P. May 12 '23 at 11:07
  • Yes it is. The problem in the above code is that I missed to pass the config to the method. `const signedURLconfig = { action: 'read', expires: '01-01-2030' };` I’ll adapt this answer accordingly because you also need a specific IAM role. – Renaud Tarnec May 12 '23 at 12:24
  • Ok, no worries. I just read that [getSignedUrl](https://googleapis.dev/nodejs/storage/latest/Bucket.html#getSignedUrl) needs a parameter. So yes. I'm waiting for your adaptation. Thank you. – Joan P. May 12 '23 at 12:27
  • Why are you returning true in try and in the other answer null? Does it make any difference? Btw, I tried your new changed code, I enabled "IAM Service Account Credentials API" and now I get `Error: Permission 'iam.serviceAccounts.signBlob' denied on resource (or it may not exist)`. Any idea? – Joan P. May 12 '23 at 13:03
  • It does not make any difference, an async function always returns a Promise. – Renaud Tarnec May 12 '23 at 13:04
  • 1
    Exactly what you wrote in your adapted answer. Let me try and provide feed-back. Thank you. – Joan P. May 12 '23 at 13:04
  • Without the the last part of your answer, `HOWEVER, you would face..., it should work for at least a day. Correct? – Joan P. May 12 '23 at 13:15
  • 1
    Yes it is correct you can test just bay assigning the `iam.serviceAccounts.signBlob` IAM permission. – Renaud Tarnec May 12 '23 at 13:48