I'm fairly new with Firebase functions and I'm trying to create a simple onCreate() trigger however I cant seem to get it up and running.
Am I not returning the promise correctly with Sendgrid? Not sure what I am missing
const functions = require("firebase-functions");
const admin = require("firebase-admin");
const sendGrid = require("#sendgrid/mail");
admin.initializeApp();
const database = admin.database();
const API_KEY = '';
const TEMPLATE_ID = '';
sendGrid.setApiKey(API_KEY);
const actionCodeSettings = {
...
};
exports.sendEmailVerify = functions.auth.user().onCreate((user) => {
admin
.auth()
.generateEmailVerificationLink(user.email, actionCodeSettings)
.then((url) => {
const msg = {
to: user.email,
template_id: TEMPLATE_ID,
dynamic_template_data: {
subject: "test email",
name: name,
link: url,
},
};
return sendGrid.send(msg);
})
.catch((error) => {
console.log(error);
});
});
Logs from firebase functions
sendEmailVerify
Function execution started
sendEmailVerify
Function returned undefined, expected Promise or value
sendEmailVerify
Function execution took 548 ms, finished with status: 'ok'
sendEmailVerify
{ Error: Forbidden
sendEmailVerify
at axios.then.catch.error (node_modules/#sendgrid/client/src/classes/client.js:133:29)
sendEmailVerify
at process._tickCallback (internal/process/next_tick.js:68:7)
sendEmailVerify
code: 403,
sendEmailVerify
message: 'Forbidden',
You are not correctly returning the Promises chain in your Cloud Function. You should do as follows:
exports.sendEmailVerify = functions.auth.user().onCreate((user) => {
return admin // <- See return here
.auth()
.generateEmailVerificationLink(user.email, actionCodeSettings)
.then((url) => {
const msg = {
to: user.email,
template_id: TEMPLATE_ID,
dynamic_template_data: {
subject: "test email",
name: name,
link: url,
},
};
return sendGrid.send(msg);
})
.catch((error) => {
console.log(error);
return null;
});
});
There are at least two programming problems here.
You're not returning a promise from the function that resolves when all the async work is complete. This is a requirement. Calling then and `catch is not sufficient. You actually have a return a promise from the function handler.
You're calling sendGrid.send(email), but you never defined a variable email anywhere in the code. If this is the case, then you're passing an undefined value to sendgrid.
There is also the possibility that your project is not on a payment plan, in which case, the call to sendgrid will always fail due to lack of outbound networking on the free plan. You will need to be on a payment plan for this to work at all.
Related
I want to securely create a user document onCreate that is in sync with the auth.user database in Firebase v9.
I think it wouldn't be secure to let a registered user create a user document. So I wrote a cloud function which triggers on functions.auth.user().onCreate() and creates a user document.
Problem:
I have the problem keeping them in sync as the onSnapshotmethod which should await for the user document to exists already returns a promise if the user document does not yet exists. Sometimes it works and sometimes not. So I don't know when I can update the by the cloud function created user document.
Question:
Why does the onSnapshot sometimes work and sometimes not. How can I fix it?
Here is a link to a helpful Article which seem to doesn't work in v9. Link
I tried and searched everywhere. I can't believe this is not a standard feature and still a requested topic. This seems so basic.
Error
error FirebaseError: No document to update: as const user = await createAccount(displayName, email, password); returns even if user is not yet in doc.data()
Sign Up function
interface SignUpFormValues {
email: string;
password: string;
confirm: string;
firstName: string;
lastName: string;
}
const createAccount = async (
displayName: string,
email: string,
password: string
) => {
// Create auth user
const userCredential = await createUserWithEmailAndPassword(
auth,
email,
password
);
// -> Signed in
// Update Profile
const user = userCredential.user;
const uid = user.uid;
await updateProfile(user, {
displayName: displayName,
});
// IMPORTANT: Force refresh regardless of token expiration
// auth.currentUser.getIdToken(true); // -> will stop the onSnapshot function from resolving properly
// Build a reference to their per-user document
const userDocRef = doc(db, "users", uid);
return new Promise((resolve, reject) => {
const unsubscribe = onSnapshot(userDocRef, {
next: (doc) => {
unsubscribe();
console.log("doc", doc); // -> returning undefined
console.log("doc.data()", doc.data()); // -> returning undefined
resolve(user); // -> returning undefined
},
error: (error) => {
unsubscribe();
console.log("error", error);
reject(error);
},
});
});
};
const handleSignUp = async (values: SignUpFormValues) => {
const { firstName, lastName, email, password } = values;
const displayName = `${firstName} ${lastName}`;
try {
setError("");
setLoading(true);
// Create user account
const user = await createAccount(displayName, email, password);
console.log("createAccount -> return:", user); // -> problem here sometimes return undefined
// Update user
const newUserData = {
displayName: displayName,
firstName,
lastName,
};
// Build a reference to their per-user document
const userDocRef = doc(db, "users", user.uid);
await updateDoc(userDocRef, newUserData);
// Send Email verification
await authSendEmailVerification(user);
// Logout
await logout();
navigate("/sign-up/email-verification", { state: values });
} catch (error: any) {
const errorCode = error.code;
const errorMessage = error.message;
console.log("error", error);
console.log("error", errorCode);
if (errorCode === "auth/email-already-in-use") {
const errorMessage =
"Failed to create an account. E-Mail address is already registered.";
setError(errorMessage);
console.log("error", errorMessage);
} else {
setError("Failed to create account.");
}
}
setLoading(false);
};
Cloud function which triggers the user onCreate
// On auth user create
export const authUserWriteListener = functions.auth
.user()
.onCreate(async (user, context) => {
console.log("user:", user);
const userRef = db.doc(`users/${user.uid}`);
await userRef.set({
email: user.email,
createdAt: context.timestamp,
firstTimeLogin: true,
});
return db.doc("stats/users").update({
totalDocsCount: FieldValue.increment(1),
});
});
The issue is that the Cloud Function code runs asynchronously. There is no guarantee that it will run quickly enough to have the document created in Firestore between the end of createAccount() and your call to updateDoc(). In fact, if your system has been idle for a while it could be a minute (or more!) for the Cloud Function to execute (do a search for "cold start firebase cloud functions").
One option, depending on your design, might be to not take in first name and last name during sign up? But instead take the user to a "profile page" once they are logged in where they could modify aspects of their profile (by that time the user profile document hopefully is created). On that page, if the get() returns no document, you could put up a notification to the user that the system "is still processing their registration" or something like that.
Unable to sendEmail after firestore document creation. I am trying to send a notification email to webapp admin's email once the document on firestore is created. However facing following issues.
index.js
const functions = require('firebase-functions');
const admin = require("firebase-admin")
const nodemailer = require('nodemailer');
admin.initializeApp()
//google account credentials used to send email
var transporter = nodemailer.createTransport({
host: 'smtp.gmail.com',
port: 465,
secure: true,
auth: {
user: '*****#gmail.com',
pass: '******'
}
});
exports.sendEmail = functions.firestore
.document('stories/{sId}')
.onCreate((snap, context) => {
const mailOptions = {
from: `*******#gmail.com`,
to: snap.data().email,
subject: 'contact form message',
html: `<h1>Order Confirmation</h1>
<p>
<b>Email: </b>${snap.data().email}<br>
</p>`
};
return transporter.sendMail(mailOptions, (error, data) => {
if (error) {
return res.send(error.toString());
}
var data = JSON.stringify(data)
return res.send(`Sent! ${data}`);
});
});
Firebase Functions logs
*sendEmail
Billing account not configured. External network is not accessible and quotas are severely limited. Configure billing account to remove these restrictions
sendEmail
Function returned undefined, expected Promise or value*
I am assuming it is safe to ignore the Billing message in logs as it is not mandatory to have a billing plan ?
Any help would be much appreciated.
The error message is telling you that your code returned something other than a promise or value as required. The problem is the way you're using transporter.sendMail(). According to the nodemailer documentation, sendMail will only return a promise if you don't pass a callback method (which you are doing) otherwise it returns undefined. So your function is returning undefined.
What you should do instead remove the callback parameter and instead handle the results from the returned promise. You can also return the promise from the function.
return transporter.sendMail(mailOptions)
.then(data => {
// decide what you want to do on success
})
.catch(err => {
// decide what you want to do on failure
});
I've made a firebase function which every time I pass data to it and try to use the data, it returns that the data is undefined. This is the function I made:
const functions = require('firebase-functions');
// The Firebase Admin SDK to access Cloud Firestore.
const admin = require('firebase-admin');
// CORS Express middleware to enable CORS Requests.
const cors = require('cors')({origin: true});
admin.initializeApp();
exports.addUser = functions.https.onRequest((req, res) => {
const handleError = (error) => {
console.log('Error creating new user:', error);
//sends back that we've been unable to add the user with error
return res.status(500).json({
error: err,
});
}
try {
return cors(req, res, async () => {
console.log(req);
const uid = req.uid;
const dob = req.dob;
const postcode = req.postcode;
const sex = req.sex;
const username = req.username;
admin.firestore().collection('users').doc(uid).set({
dob:dob,
postcode:postcode,
sex:sex,
username:username,
})
.then(function(userRecord) {
console.log('Successfully created new user:', userRecord.username);
// Send back a message that we've succesfully added a user
return res.status(201).json({
message: 'User stored',
id: req.body.uid,
});
})
.catch(function(error) {
return handleError(error);
});
});
} catch (error) {
return handleError(error);
}
});
This is how I call it within react:
const addUserFunc = firebase.functions().httpsCallable('addUser');
console.log("Calling user func " + user.uid)
addUserFunc({
uid:user.uid,
dob:dob,
postcode:postcode,
sex:sex,
username:username,
}).then(function(result) {
console.log(result);
}).catch(err => {
console.log(err)
setErrors(prev => ([...prev, err.message]))
});
I've printed the data before sending the request and it definitely exists. I've also tried getting it within the function using req.body and req.query but this just returns the same.
This is the error I get in the firebase function logs:
Error: Value for argument "document path" is not a valid resource path. The path must be a non-empty string.
at Object.validateResourcePath (/srv/node_modules/#google-cloud/firestore/build/src/path.js:406:15)
at CollectionReference.doc (/srv/node_modules/#google-cloud/firestore/build/src/reference.js:1982:20)
at cors (/srv/index.js:44:51)
at cors (/srv/node_modules/cors/lib/index.js:188:7)
at /srv/node_modules/cors/lib/index.js:224:17
at originCallback (/srv/node_modules/cors/lib/index.js:214:15)
at /srv/node_modules/cors/lib/index.js:219:13
at optionsCallback (/srv/node_modules/cors/lib/index.js:199:9)
at corsMiddleware (/srv/node_modules/cors/lib/index.js:204:7)
at exports.addUser.functions.https.onRequest (/srv/index.js:31:16)
This is the error return in the web console for the react app:
Access to fetch at 'https://***/addUser' from origin 'http://localhost:3000' 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.
I tested the function within using the emulator and passing the values using the link which works there but just not when deployed.
Any help would be great.
Your Cloud Function is defined as a HTTPS Function, which means that you can access it over a URL, but then you're calling it from your code as a Callable Function. The two types are different and not interchangeable.
If you want to use the firebase.functions().httpsCallable('addUser'); in your client code, you'll have to modify your Cloud Function to be a Callable Function too. This mostly means that you get the parameters from data instead of res, and return responses instead of sending them through res.
exports.addUser = functions.https.onCall((data, context) => {
...
const uid = context.auth.uid; // automatically passed to Callable Functions
return admin.firestore().collection('users').doc(uid).set({
dob: data.dob,
postcode: data.postcode,
sex: data.sex,
username: data.username,
})
.then(function(userRecord) {
return {
message: 'User stored',
id: req.body.uid,
};
}).catch(err => {
throw new functions.https.HttpsError('dabase-error', error);
})
});
Alternatively, you can leave your Cloud Function as is and instead modify the calling code to use something like fetch().
i have this issue with a firebase app i'm also developing , my intentions are to create an user and update his profile with a name using the firebase method createUserWithEmailAndPassword.
I evolve the process and eventually it works but also throws an error which says kind of :
Uncaught TypeError: Cannot read property user of undefined
at eval (index.js?xxxx)
at e.g (auth.esm.js?xxx)
at kc (auth.esm.js?xxxxx)
at gc (auth.esm.js?xxxxxx)
at B.k.Zb (auth.esm.js?xxxxxx)
at Qb (auth.esm.js?xxxx)
despite of having already modified the user info, then i need to refresh the page to get this error to dissapear.
Here part of my code:
signUserUp({ commit }, payload) {
commit("settingLoader", true);
firebase
.auth()
.createUserWithEmailAndPassword(payload.email, payload.password)
.then(() => {
let user = firebase.auth().currentUser;
console.log(user);
user
.updateProfile({
displayName: payload.name
})
.then(usermod => {
const User = {
id:usermod.user.uid, undefined usermod
email:usermod.user.email, undefined usermod
name:usermod.user.displayName undefined usermod
};
commit("settingUserIn", User);
commit("settingLoader", false);
});
})
.catch(error => {
console.log(error);
commit("settingLoader", false);
});
}
Then the error does reference to an eventual undefined "usermod" for user.uid, user.displayName ,and user.email.
Any advice about what i'm missing?
thanks in advance!!
https://firebase.google.com/docs/reference/js/firebase.User#updateprofile
Firebase's user.updateProfile method returns a void promise, meaning it returns a promise with no value.
You still have access to your user variable in your then, so why not just change it to
...
user
.updateProfile({
displayName: payload.name
})
.then(() => {
const User = {
id: user.uid,
email: user.email,
name:user.displayName
};
commit("settingUserIn", User);
commit("settingLoader", false);
});
...
I've created a callable function from my Angular component. My angular component calls the createUser function and successfully returns the userRecord value.
However, what I would like to do is call another cloud function called createUserRecord. I'm not overly familiar with promises and what needs to be returned where in this particular scenario.
Below are my two cloud functions. How would I go about calling createUserRecord upon the success of createUser?
export const createUser = functions.https.onCall(async (data, context) => {
console.log('data = ', data);
return admin.auth().createUser({
email: data.email,
password: data.password,
}).then(function (userRecord) {
return userRecord
})
.catch(function (error) {
return error;
console.log('Error creating new user:', error);
});
});
export const createUserRecord = functions.auth.user().onCreate((user, context) => {
const userRef = db.doc(`users/${user.uid}`);
return userRef.set({
email: user.displayName,
createdAt: context.timestamp,
nickname: 'bubba',
})
});
Update
This is a version I produced where I merged the two functions together. This does produce the expected result of creating and account and then writing to firestore. However, it does feel a little 'off' due to the fact that it doesn't return a value to the client.
export const createUser = functions.https.onCall(async (data, context) => {
console.log('data = ', data);
return admin.auth().createUser({
email: data.email,
password: data.password,
}).then(function (userRecord) {
const userRef = db.doc(`users/${userRecord.uid}`);
return userRef.set({
email: data.email,
name: data.name,
})
})
.catch(function (error) {
return error;
console.log('Error creating new user:', error);
});
});
Angular Callable Function
The sanitizedMessage console log will return undefined.
addUser() {
const createUser = firebase.functions().httpsCallable('createUser');
const uniquePassword = this.afs.createId();
createUser({
email: this.userForm.value.email,
password: uniquePassword,
name: this.userForm.value.name,
}).then((result) => {
// Read result of the Cloud Function.
var sanitizedMessage = result.data.text;
console.log('sanitizedMessage = ', sanitizedMessage);
}).catch((error) => {
var code = error.code;
var message = error.message;
var details = error.details;
console.log('error = ', error);
});
}
If you want to create a record in Firestore upon user creation you can very well do that within a unique Cloud Function. The following code will do the trick, making the assumption that you want to write to the users Firestore collection.
const FieldValue = require('firebase-admin').firestore.FieldValue;
...
export const createUser = functions.https.onCall((data, context) => {
console.log('data = ', data);
return admin
.auth()
.createUser({
email: data.email,
password: data.password
})
.then(userRecord => {
return admin.firestore().collection('users')
.doc(userRecord.uid)
.set({
email: userRecord.displayName,
createdAt: FieldValue.serverTimestamp(),
nickname: 'bubba'
});
})
.then(() => {
return {
result: 'Success'
};
})
.catch(error => {
//Look at the documentation for Callable Cloud Functions to adapt this part:
//https://firebase.google.com/docs/functions/callable?authuser=0
});
});
"Is there any particular reason not to chain functions in CF's?" ?
As explained in the documentation, "Cloud Functions can be associated with a specific trigger". You can "chain" Cloud Functions by creating the corresponding triggers, for example, creating a doc in Firestore in one CF (Callable Function for example), that would trigger another CF that respond to a Firestore trigger). Having said that, in most cases you can probably cover a lot of needs in a unique Cloud Function, by chaining promises, instead of chaining Cloud Functions.
Finally, I would not recommend to call an HTTP Cloud Function from within a Cloud Function because (IMHO) HTTP Cloud Functions are more designed to be called by an external consumer (I even don't know if this would work).
It would be interesting to have Firebasers opinion on that!