Firestore transaction and batch? - javascript

I have a function that register a client. The function takes care of creating the user, the users company, and adding some meta data about the user. However, the fragile thing in the function is that if one of the requests fails, the user is stuck and can't sign up again - I was told that I could solve this by using a transactions or batch. I've looked into writeBatch() and batches, but it seems you need to have a reference to a document that you want to update. However, in my case, I'm creating new documents - How can i refactor this into be a transaction/batches? What I have so far is this:
const registerUser = async (user: RegisterUserData) => {
try {
const batch = writeBatch(db);
const test = await runTransaction(db, async (transaction) => {
const userDoc = await createUserWithEmailAndPassword(auth, user.email, user.password);
const userId = userDoc.user.uid;
const companyRef = batch.set(doc(db, 'companies'), {
title: user.companyName,
userCount: 1
});
let isAdmin = false;
if (await setUserAsAdmin(userId)) {
isAdmin = true;
}
batch.set(doc(db, 'users'), {
email: user.email,
userId: userId,
firstName: user.firstName,
lastName: user.lastName,
isAdmin: isAdmin,
role: isAdmin ? 'admin' : 'coach',
clientCount: 0,
// companyId: companyRef.id
});
await batch.commit();
});
console.log('test', test)
} catch (e) {
console.log('error', e);
throw new Error('cant register user: ' + e);
}
};
The original function before my refactor looks like this (And is working):
const registerUser = async (user: RegisterUserData) => {
try {
const createUser = await createUserWithEmailAndPassword(auth, user.email, user.password);
const createdCompany = await createUserCompany({
title: user.companyName,
userCount: 1
});
let isAdmin = false;
const adminSet = await setUserAsAdmin(createUser.user.uid);
if (adminSet) {
isAdmin = true;
}
await createUserMetaData({
email: user.email,
userId: createUser.user.uid,
firstName: user.firstName,
lastName: user.lastName,
isAdmin: isAdmin,
role: isAdmin ? 'admin' : 'coach',
clientCount: 0,
companyId: createdCompany.id
});
return createUser;
} catch (e) {
throw new Error('cant register user' + e);
}
};

You cannot include a call to the createUserWithEmailAndPassword() method from the Auth part of the JS SDK in a Firestore transaction. As a matter of fact, the set of atomic operations that you can include in a Firestore transaction can only be Firestore operations (see the doc for the exact list).
So while you can include your two Firestore writes in a batched write it is not possible to include the user creation in the transaction.
One possible solution if you want to avoid the (rare) case where the user is created but not the Firestore docs is to regularly check for this case, for example with a scheduled Cloud Function which corrects the problem.

Related

accessing the same instance of an eventEmitter across files (node.js / express / event listeners / pub-sub architecture)

I'm creating a user via the following post route
.post(async (request, response) => {
const { email, password, firstName, lastName, phoneNumber } = request.body;
console.log(String(email));
console.log(String(password));
const emailError = validateEmail(String(email));
const phoneError = validatePhoneNumber(String(phoneNumber));
if (emailError) {
return response.status(400).send(emailError);
}
if (phoneError) {
return response.status(400).send(phoneError);
}
try {
const result = await createUser(email, password, firstName, lastName, phoneNumber);
console.log("User created!");
response.send(`${firstName} ${lastName} has been added to the database! A confirmation email has been sent to ${email}`);
} catch (error) {
console.log(error);
return response.status(500).send('An error occurred while adding the user to the database');
}
})
the createUser function is in this userService.js publisher
const EventEmitter = require('events');
const userEmitter = new EventEmitter();
const createUser = async (email, password, firstName, lastName, phoneNumber) => {
userEmitter.emit('userCreated', { email, password, firstName, lastName, phoneNumber });
console.log("Emitting userCreated event...");
emmiterCheck();
};
function emmiterCheck() {
if (userEmitter.listenerCount('userCreated') > 0) {
console.log("User creation event emitting!");
} else {
console.log("User creation event not fired");
}
};
module.exports = {
createUser,
userEmitter,
};
the subscriber/listener is here in userDBService.js
const connection = require('../database/connection');
const {createUser} = require('../publishers/userService');
const uuid = require('uuid');
const User = require('../models/User');
createUser.userEmitter.on('userCreated', async (user) => {
console.log("User creation event fired!");
try {
const newUser = await User.create({
id: uuid.v4(),
email: user.email,
password: user.password,
firstName: user.firstName,
lastName: user.lastName,
phoneNumber: user.phoneNumber
});
console.log(`User with ID ${newUser.id} created`);
} catch (err) {
console.log(err);
}
});
when I hit the post route, its saying "Emitting userCreated event...
User creation event not fired
User created!" but it seems like the publisher-subscriber are not on the same instance? I'm not sure if this is the problem, but please advise. Thank you!
I wrote that emmiterCheck function to see if the listeners we're linked, and they're not. I tried creating a specific createUserEmmiter but that still didn't work.

What is the difference between passing in argument as object vs string into a function

I have below is a method that signs a user up with their email and password and create a document in Firebase when the user enters their info into a form and clicks submit:
const onSubmitHandler = async (e) => {
e.preventDefault();
try {
const { user } = await createAuthUserWithEmailAndPassword(email, password);
console.log(user, 'user')
await createUserDocumentFromAuth(user, { displayName })
}catch(err) {
if(err === 'auth/email-already-in-use') {
alert('Account with this email already exists');
return;
}else {
console.log(err)
}
}
}
For this function call:
await createUserDocumentFromAuth(user, { displayName })
where displayName can be a string, such as ElonMusk.
In the actual createUserDocumentFromAuth, I am calling the setDoc method, which is one from Firebase to set a user document:
export const createUserDocumentFromAuth = async ( userAuth, additionalInfo = {} ) => {
if(!userAuth) return;
console.log(additionalInfo, 'additionalInfo')
const userDocRef = doc(db, 'users', userAuth.uid);
const userSnapshot = await getDoc(userDocRef);
if(!userSnapshot.exists()) {
const { displayName, email } = userAuth;
const createdAt = new Date();
try {
// set the doc here
await setDoc(userDocRef, {
displayName,
email,
createdAt,
...additionalInfo
});
} catch(err) {
console.log('err creating the user', err)
}
};
return userDocRef;
}
The reason I passed { displayName } in manually is because there is a case where the server's response to createAuthUserWithEmailAndPassword(email, password) has displayName to be null, but we want the user to have a displayName registered in the database.
My question is:
Why does displayName only work when it is passed in as an object and not just in its normal form? For example:
await createUserDocumentFromAuth(user, { displayName })
will replace the displayName: null
But not when I pass in:
await createUserDocumentFromAuth(user, displayName)
What is this technique called in JavaScript?
If you look into createUserDocumentFromAuth, you'll see that it's expects two arguments userAuth and additionalInfo, both arguments are expected to be objects.
It later uses data from additionalInfo to add/overwrite anything in userAuth when calling setDoc() method.
So, I'd recommend add
console.log(userDocRef, {
displayName,
email,
createdAt,
...additionalInfo
});
to see what what data is being sent to setDoc()

FirebaseError: Function Query.where() called with invalid data. Unsupported field value: undefined

I want to get only the document specified by where.
Store the email address registered in the uid property as a value in the Firestore at the time of new registration.
async signUp() {
const db = firebase.firestore();
await this.$store.dispatch('signUp', { username:this.username, email:this.email, password:this.password });
await db.collection('myData').doc(this.username).set({
uid: this.email,
email: this.email,
myWallet: 1000
});
this.$router.push('/home');
}
async signUp({ commit }, userInfomation) {
try {
await firebase
.auth()
.createUserWithEmailAndPassword(
userInfomation.email,
userInfomation.password
);
const user = firebase.auth().currentUser;
await user.updateProfile({
displayName: userInfomation.username,
});
commit('setUserName', user);
} catch (e) {
alert(e.message);
}
},
mutations: {
setUserName(state, user) {
state.userName = user.displayName;
},
actions: {
async getMyWallet({ commit, state }) {
const uid = state.updateUserName.email; //Information on the logged-in user is stored in updateUserName.
const db = firebase.firestore();
const doc = await db
.collection('myData')
.where('uid', '==', uid)
.get();
commit('getMyWallet', doc);
}
}
When I ran it, I got the error written in the title.
Is it written incorrectly?
Is there any other good way?
async getMyWallet({ commit }) {
firebase.auth().onAuthStateChanged(user => {
const uid = user.email;
const db = firebase.firestore();
const doc = db
.collection('myData')
.where('uid', '==', uid)
.get();
commit('getMyWallet', doc);
});
},
When I rewrote the getMyWallet part, I got a new error.
Error details: doc.data is not a function

Create user with firebase admin sdk that can signIn using email and password

I'm using firebase admin SDK on cloud functions to create users using
admin.auth().createUser({
email: someEmail,
password: somePassword,
})
now I want user to signIn using signInWithEmailAndPassword('someEmail', 'somePassword') but I cannot.
I get the following error
{code: "auth/user-not-found", message: "There is no user record corresponding to this identifier. The user may have been deleted."}
There doesn't seem to be a reason to Stringify/Parse. This worked after I struggled with an unrelated typo...
FUNCTION CALL FROM REACT JS BUTTON CLICK
<Button onClick={() => {
var data = {
"email": "name#example.com",
"emailVerified": true,
"phoneNumber": "+15551212",
"password": "randomPW",
"displayName": "User Name",
"disabled": false,
"sponsor": "Extra Payload #1 (optional)",
"study": "Extra Payload #2 (optional)"
};
var createUser = firebase.functions().httpsCallable('createUser');
createUser( data ).then(function (result) {
// Read result of the Cloud Function.
console.log(result.data)
});
}}>Create User</Button>
And in the index.js in your /functions subdirectory:
const functions = require("firebase-functions");
const admin = require('firebase-admin');
admin.initializeApp();
// CREATE NEW USER IN FIREBASE BY FUNCTION
exports.createUser = functions.https.onCall(async (data, context) => {
try {
const user = await admin.auth().createUser({
email: data.email,
emailVerified: true,
password: data.password,
displayName: data.displayName,
disabled: false,
});
return {
response: user
};
} catch (error) {
throw new functions.https.HttpsError('failed to create a user');
}
});
Screen shot of console output
In 2022 there still is no method built into the Admin SDK that would allow to create users in the emulator.
What you can do is to use the REST API of the emulator to create users there directly. The API is documented here: https://firebase.google.com/docs/reference/rest/auth#section-create-email-password
Provided you have got and nanoid installed you can use the following code to create users in the emulator.
import { nanoid } from 'nanoid'
import httpClientFor from '../lib/http-client/client.js'
const httpClient = httpClientFor('POST')
export const createTestUser = async ({ email = `test-${nanoid(5)}#example.io`, password = nanoid(10), displayName = 'Tony' } = {}) => {
const key = nanoid(31)
const { body: responseBody } = await httpClient(`http://localhost:9099/identitytoolkit.googleapis.com/v1/accounts:signUp?key=${key}`, {
json: {
email,
password,
displayName
}
})
const responseObject = JSON.parse(responseBody)
const { localId: userId, email: userEmail, idToken, refreshToken } = responseObject
return { userId, userEmail, idToken, refreshToken }
}
Please note: As there is no error handling implemented, this snippet is not suitable for production use.
Try like that
And please be ensure that user is created from the panel
admin.auth().createUser({
email: "user#example.com",
emailVerified: false,
phoneNumber: "+11234567890",
password: "secretPassword",
displayName: "John Doe",
photoURL: "http://www.example.com/12345678/photo.png",
disabled: false
})
.then(function(userRecord) {
// See the UserRecord reference doc for the contents of userRecord.
console.log("Successfully created new user:", userRecord.uid);
})
.catch(function(error) {
console.log("Error creating new user:", error);
});
Just in case anyone else comes across this I was able to fix it with the help of this.
Here is a working example inside of an onCreate cloud function:
exports.newProjectLead = functions.firestore
.document('newProjectForms/{docId}')
.onCreate(async (snapshot) => {
const docId = snapshot.id
// this is what fixed it the issue
// stringify the data
const data = JSON.stringify(snapshot.data())
// then parse it back to JSON
const obj = JSON.parse(data)
console.log(obj)
const email = obj.contactEmail
console.log(email)
const password = 'ChangeMe123'
const response = await admin.auth().createUser({
email,
password
})
data
const uid = response.uid
const dbRef = admin.firestore().collection(`clients`)
await dbRef.doc(docId).set({
id: docId,
...data,
uid
}, {
merge: true
})
console.log('New Client Created')
})

Using async in app startup script not returning any results

I am trying to run the following script in my Node app to check if any users exist and if not, create first admin user. Yet the script simply do nothing, return nothing even while using Try/Catch so can someone please tell me what I am missing / doing wrong here? or how I can possibly catch the error (if any)? Thanks
import pmongo from 'promised-mongo';
import crypto from 'crypto';
const salt = 'DuCDuUR8yvttLU7Cc4';
const MONGODB_URI = 'mongodb://localhost:27017/mydb';
const db = pmongo(MONGODB_URI, {
authMechanism: 'ScramSHA1'
}, ['users']);
async function firstRunCheckAndCreateSuperAdmin(cb) {
const username = 'admin2#test2.com';
try {
const user = await db.users.findOne({ role: 'admin'});
console.log(user);
if(!user) return cb('No user found');
} catch(e) {
cb('Unexpected error occurred');
}
if(!user) {
console.log('No admin detected.');
const adminPassword = crypto.pbkdf2Sync ( 'password', salt, 10000, 512, 'sha512' ).toString ( 'hex' );
await db.users.update({username: username}, {$set: {username: username, password: adminPassword, role: 'admin'}}, {upsert: true});
}
db.close();
process.exit();
}
firstRunCheckAndCreateSuperAdmin(function(err, resultA){
if(err) console.log(err);
});
You are not returning any callback when there is no admin user in the following code snippet
if (!user) {
console.log('No admin detected.');
const adminPassword = crypto.pbkdf2Sync ( 'password', salt, 10000, 512, 'sha512' ).toString ( 'hex' );
await db.users.update({username: username}, {$set: {username: username, password: adminPassword, role: 'admin'}}, {upsert: true});
// call cb(user) here
}
Please see comment.
import pmongo from 'promised-mongo';
import crypto from 'crypto';
const salt = 'DuCDuUR8yvttLU7Cc4';
const MONGODB_URI = 'mongodb://localhost:27017/mydb';
const db = pmongo(MONGODB_URI, {
authMechanism: 'ScramSHA1'
}, ['users']);
async function firstRunCheckAndCreateSuperAdmin(cb) {
const username = 'admin2#test2.com';
try {
const user = await db.users.findOne({
role: 'admin'
});
console.log(user);
//(1) If user is undefined, then launch cb with an error message;
if (!user) return cb('No user found');
} catch (e) {
//(2) If something is wrong, then launch cb with an error message;
cb('Unexpected error occurred');
}
//This part of the code will only be reached if user is defined.
//This is a dead code as if user is undefined, it would have exited at (1)
if (!user) {
console.log('No admin detected.');
const adminPassword = crypto.pbkdf2Sync('password', salt, 10000, 512, 'sha512').toString('hex');
await db.users.update({
username: username
}, {
$set: {
username: username,
password: adminPassword,
role: 'admin'
}
}, {
upsert: true
});
}
//So if user exists, it will close db and exit without calling cb.
db.close();
process.exit();
}
firstRunCheckAndCreateSuperAdmin(function(err, resultA) {
if (err) console.log(err);
});
Note:
If you are using async/await, then you don't need to use callback.
If you are using callback, then you don't need to have a return statement.
If the intention of the function is suppose to have a return value, make sure all code path returns a value.
I have tried to rewrite your code to make it smaller and to remove all node-style callback types of async code from it. I replaced update with insertOne since you only have one user to insert (not multiple to update). Also I have added 500 ms timeout when calling firstRunCheckAndCreateSuperAdmin in case it "hangs". It should log something at the end :)
import pmongo from 'promised-mongo'
import crypto from 'crypto'
import {
promisify
} from 'util'
const pbkdf2 = promisify(crypto.pbkdf2)
const salt = 'DuCDuUR8yvttLU7Cc4'
const MONGODB_URI = 'mongodb://localhost:27017/mydb'
const db = pmongo(MONGODB_URI, {
authMechanism: 'ScramSHA1'
}, ['users']);
const username = 'admin2#test2.com'
async function firstRunCheckAndCreateSuperAdmin() {
let user = await db.users.findOne({
role: 'admin'
});
if (!user) { // no user lets create one
user = await db.users.insertOne({
username: username,
password: (await pbkdf2('password', salt, 10000, 512, 'sha512')).toString('HEX'),
role: 'admin'
});
}
return user
}
const timeout = delay => message => new Promise((_, reject) => setTimeout(reject, delay, new Error(message)))
Promise
.race([firstRunCheckAndCreateSuperAdmin(), timeout(500)('Rejected due to timeout')])
.then(user => console.log(`Got user ${JSON.stringify(user)}`))
.catch(error => console.error(error))

Categories

Resources