Unable to add data to custom User collection properly in Firestore - javascript

Problem
I'd like to add several user data to firestore Authenticateced user list AND to users collection which I created by myself at same time, but it does't go well. users collection are updated only its part of it.
Data
// javascript
users: [
{email: "n_0#example.com", username: "user0"},
{email: "n_1#example.com", username: "user1"},
{email: "n_2#example.com", username: "user2"},
{email: "n_3#example.com", username: "user3"},
{email: "n_4#example.com", username: "user4"}
]
Code
// javascript
import * as app from 'firebase/app'
import 'firebase/auth'
const config = JSON.parse(process.env.VUE_APP_FIREBASE_CONFIG)
app.initializeApp(config)
export const firebase = app
export const auth = app.auth()
function asyncCreateUser(user) {
return auth.createUserWithEmailAndPassword(
user.email,
'password'
).then(function (createdUser) {
console.log('---')
console.log('user.email', user.email)
console.log('createdUser', createdUser.user.email)
const ref = usersRef.doc(createdUser.user.uid)
return ref.set(user)
})
}
this.users.map(user => asyncCreateUser(user))
Result
Authenticated users are ok.
users collection has problem: it has only three users in the collection. The number of users added to user collection may differ in different execution.
Log
Debug.vue?2083:50 user.email n_3#example.com
Debug.vue?2083:51 createdUser n_2#example.com
Debug.vue?2083:49 ---
Debug.vue?2083:50 user.email n_2#example.com
Debug.vue?2083:51 createdUser n_1#example.com
Debug.vue?2083:49 ---
Debug.vue?2083:50 user.email n_1#example.com
Debug.vue?2083:51 createdUser n_1#example.com
Debug.vue?2083:49 ---
Debug.vue?2083:50 user.email n_0#example.com
Debug.vue?2083:51 createdUser n_0#example.com
Debug.vue?2083:49 ---
Debug.vue?2083:50 user.email n_4#example.com
Debug.vue?2083:51 createdUser n_4#example.com
It's strange that in some section, user.email and createdUser are diffrent.
Help wanted
I'd like to know how to fix it. If possible, I'd like to know the causes too. Thank you!

Just a guess - you are using the javascript SDK to create the user, not the admin SDK.
The Javascript SDK logs the user in after creating the account, so basically you rapidly logged a new account in and out 5 times in a row, hence the mix up with the user ids when creating the firestore documents - it can be that you were just logged out at that moment:
If the new account was created, the user is signed in automatically.
Have a look at the Next steps section below to get the signed in user
details.
Firebase docs
If you want to bulk-create user accounts you are better off using the admin SDK in a secure environment (e.g. cloud functions) and simply trigger the https function from your frontend. The way you are doing it now means that all the accounts will be created sequentially which can be quite time consuming when you create lots at once - if you are using a cloud function and the admin sdk you can kick off the creation of all accounts in parallel and return a promise once all are finished - something along the lines of:
return Promise.all(users.map(user => admin.auth().createUser(user)
.then(function(userRecord) {
return admin.firestore().collection('users').doc(userRecord.uid).set(...);
})
})
Firebase admin reference

I solved it by my self. It seems async call in map made something wrong.
self = this
for (let i = 0; i < 5; i++) {
const self = this
await auth.createUserWithEmailAndPassword(data[i].email, 'password')
.then(function (res) {
const ref = usersRef.doc(res.user.uid)
self.data[i]._id = res.user.uid
ref.set(self.data[i])
}
)
}

Related

Firebase: Re-authenticate the current user inside a Cloud Function

I am implementing a cloud function for updating the current user's password.
Basically, the logic I want to follow is:
(Client side)
0. Complete form and submit the data (current password and new password).
(Backend)
1. Get the current user email from the callable function context.
2. Re-authenticate the current user using the provided current password.
2.1. If success, change the password and send a notification email.
2.2. Else, throw an error.
Here is my current code:
const { auth, functions } = require("../../services/firebase");
...
exports.updatePassword = functions
.region("us-central1")
.runWith({ memory: "1GB", timeoutSeconds: 120 })
.https.onCall(async (data, context) => {
const { currentPassowrd, newPassword } = data;
const { email, uid: userId } = context.auth.token;
if (!userId) {
// throw ...
}
try {
//
// Problem: `firebase-admin` authentication doesn't include
// the `signInWithEmailAndPassword()` method...
//
await auth.signInWithEmailAndPassword(email, currentPassowrd);
await auth.updateUser(userId, {
password: newPassword,
});
sendPasswordUpdateEmail(email);
} catch (err) {
// ...
throw AuthErrors.cannotUpdatePassword();
}
});
My problem is that the firebase-admin package doesn't include the signInWithEmailAndPassword, and I need a way to handle this, to check that "currentPassword" is correct, inside my function.
My other option, if the one I have described is not possible, is to update the password using the firebase sdk in the client side, and then to call a firebase function to send the notification email.
Strictly speaking you don't need to re-authenticate the user in the Cloud Function: If you get a value for context.auth.uid in your Callable Cloud Function, it means that the user is authenticated in the front-end and you can therefore safely call the updateUser() method.
If you want to deal with the case when the user left his device opened, and someone updates his password, as explained in the comments under your question, I would suggest you use the reauthenticateWithCredential() method in the front-end, which re-authenticates a user using a fresh credential.
Do as follows:
import {
EmailAuthProvider,
getAuth,
reauthenticateWithCredential,
} from 'firebase/auth'
const email = auth.currentUser.email;
// Capture the password value
// e.g. via a pop-up window
const password = ...;
const auth = getAuth();
const credential = EmailAuthProvider.credential(
email,
password
);
await reauthenticateWithCredential(
auth.currentUser,
credential
);
// If no error is thrown, you can call the Callable Cloud Function, knowing the user has just re-signed-in.

How to create user auth without closing the current firebase session [duplicate]

This question already has answers here:
Firebase kicks out current user
(19 answers)
Closed 1 year ago.
I want to make a system where the administrator can create user auth from an email. I have developed as the documentation says but the current session is closed. I only want to create the auth to get the uid and then create a user in the database with the data I want to store.
This is what I have:
var email = emailInput.value;
var password = "Abcd1234";
firebase.auth().createUserWithEmailAndPassword(email, password).then((userCredential) => {
var user = userCredential.user;
//user.uid contains the id I want to create the instance on ref usuarios
database.ref("usuarios/"+ user.uid).set({...});
});
Edit:
You cannot create new users using client SDK. By that I mean a user creating new users as required. You need to use Firebase Admin SDK (which must be in a secure server environment - like Firebase Cloud Functions).
You can write a cloud function like this:
exports.createNewUser = functions.https.onCall((data, context) => {
if (isAdmin(context.auth.uid)) {
return admin.auth().createUser({
email: data.email,
password: data.password,
displayName: data.name
}).then((userRecord) => {
// See the UserRecord reference doc for the contents of userRecord.
console.log('Successfully created new user:', userRecord.uid);
return { uid: userRecord.uid }
}).catch((error) => {
console.log('Error creating new user:', error);
return { error: "Something went wrong" }
});
}
return {error: "unauthorized" }
})
Now there are multiple ways you could verify that the user who is calling this function is an admin. First one would be using Firebase Custom Claims which are somewhat like roles you assign to users. Another option would be storing UID of using in database and checking the UID exists in admin node of db. Just make sure only you can edit that part of the database.
To call the function from client:
const createNewUser = firebase.functions().httpsCallable('createNewUser');
createNewUser({ name: "test", email: "test#test.test", password: "122345678" })
.then((result) => {
// Read result of the Cloud Function.
var response = result.data;
});

Firebase Security Rule - Access a field in other document

Introduction
I have this structure on my db
C- usernames
D- paola
-> userId: 7384-aaL732-8923dsnio92202-peesK
D- alex
-> userId: ...
D- adam
-> userId: ...
C- users
D- userId of paola
-> username: "paola"
-> ...
D- userId of alex
-> username: "alex"
-> ...
D- userId of adam
-> username: "adam"
-> ...
I am signing up users in the client side so I have had to write some security rules...
In my client code I do:
Add the username (document id) with the userId (document data) to the usernames collection
Create a user document in the users collection with the username and other stuff.
Security Rules
So, my security rules look like this:
function isUsernameOwner(username) {
return get(/databases/$(database)/documents/usernames/$(username)).data.userId == request.auth.uid;
}
match /users/{userId} {
// Every people can read the users collection (might be in the sign in form)
allow read: if true;
// As the users creation is made in the client side, we have to make sure
// it meets these requirements
allow write: if isSignedIn() &&
isSameUser(userId) &&
request.resource.data.keys().hasOnly(['email', 'username', 'name', 'birthday']) &&
isValidUsername(request.resource.data.username) &&
isUsernameOwner(request.resource.data.username); // <------- If I remove this all works fine
}
Problem
When I try to sign up, I get "Missing or insufficent permissions"... I think the problem is in the function isUsernameOwner() but I don't know what am I doing wrong... Am I accessing incorrectly the field userId in the username document? If not, is it possible that the batched write doesn't happen sequentially?
Pd: The signup process is made using a batched write (first write the username, then the user)
UPDATE
This is the javascript code in which I make the batched write:
// Firebase.js
createUser = (email, password, username, name, birthday) => {
return this.auth
.createUserWithEmailAndPassword(email, password)
.then((currentUser) => {
// Get the user id
const userId = currentUser.user.uid;
// Get a new Firestore batched write
const batch = this.db.batch();
// Create the new username document in the usernames collection with the user's id as field
const usernameRef = this.db.collection("usernames").doc(username);
batch.set(usernameRef, { userId });
// Create the new user document in the users collection with all the relevant information
const userRef = this.db.collection("users").doc(userId);
birthday = firebase.firestore.Timestamp.fromDate(new Date(birthday)); // It is neccessary to convert the birthday to a valid Firebase Timestamp
const data = {
email,
username,
name,
birthday,
};
batch.set(userRef, data);
// Commit the batch
return batch.commit();
})
.catch((err) => {
throw err;
});
I think the problem is that you are using get() in your security rule global function. Make it local and use getAfter instead to wait until the 'termination' of the batched write.
Here you can see a post which might be useful for your case: Firebase security rules difference between get() and getAfter()
Just see the Doug answer, he explains the differences between get and getAfer.

How to access a different collection within a firebase function

I promise i thoroughly checked through all the previous asked questions and there isn't anything similar to this. I have a firebase function that listens to onCreate on a firestore collection.
exports.sendEmail = functions.firestore.document('/Users/{documentId}')
.onCreate((snap, context) => {
const username = snap.data().username;
const email = snap.data().email;
console.log(username, email)
const mailRef = functions.firestore.document('/mail')
return mailRef.ref.set({
email: email,
subject: 'Welcome'
});
});
After a document is created in Users, i want to take the data in users and create a new document in a main collection called mail. Is this possible because i've read the docs like 10 times and there's nothing on this. Any help is highly appreciated.
To create a document in cloud functions, then you need to use the admin sdk, so first install the package:
npm install firebase-admin --save
initialize the admin sdk:
const admin = require('firebase-admin');
admin.initializeApp({
credential: admin.credential.applicationDefault()
});
const db = admin.firestore();
Then you can add:
const mailRef = db.collection('mail')
return mailRef.add({
email: email,
subject: 'Welcome'
});
https://googleapis.dev/nodejs/firestore/latest/CollectionReference.html#add
I know this is coming a bit late, just incase anyone found themselves with this same question.When you use set() to create a document, you must specify an ID for the document to create. so basically the above code just takes a little tweak as follows
exports.sendEmail = functions.firestore.document('/Users/{documentId}')
.onCreate((snap, context) => {
const username = snap.data().username;
const email = snap.data().email;
const uid = context.params.documentId //get the doc ID
const alternateUid = //you can generate a random ID here
console.log(username, email)
const mailRef = firestore.collection("mail").doc(uid)
return mailRef.set({
email: email,
subject: 'Welcome'
});
});
But sometimes there isn't a meaningful ID for the document or you don't want to use any of the above. Then it's more convenient to let Cloud Firestore auto-generate an ID for you. You can do this by calling add(), as such
const mailRef = firestore.collection("mail")
return mailRef.add({
email: email,
subject: 'Welcome'
})

Firebase Document for each user?

I am wondering how to make a document for each user as they create their account (with Firebase Web). I have Firebase Authentication enabled and working, and I'd like each user then to have a document in Cloud Firestore in a collection named users. How would I get the UID and then automatically create a document for each user? (I am doing this so that calendar events can be saved into an array field in the document, but I need a document for the user to start with). I am aware and know how to make security rules for access, I just don't know how to make the document in the first place.
Thanks!
While it is definitely possible to create a user profile document through Cloud Functions, as Renaud and guillefd suggest, also consider creating the document directly from your application code. The approach is fairly similar, e.g. if you're using email+password sign-in:
firebase.auth().createUserWithEmailAndPassword(email, password)
.then(function(user) {
// get user data from the auth trigger
const userUid = user.uid; // The UID of the user.
const email = user.email; // The email of the user.
const displayName = user.displayName; // The display name of the user.
// set account doc
const account = {
useruid: userUid,
calendarEvents: []
}
firebase.firestore().collection('accounts').doc(userUid).set(account);
})
.catch(function(error) {
// Handle Errors here.
var errorCode = error.code;
var errorMessage = error.message;
// ...
});
Aside from running directly from the web app, this code also creates the document with the user's UID as the key, which makes subsequent lookups a bit simpler.
You´ll have to set a firebase function triggered by the onCreate() Auth trigger.
1. create the function trigger
2. get the user created data
3. set the account data.
4. add the account data to the collection.
functions/index.js
// Firebase function
exports.createAccountDocument = functions.auth.user().onCreate((user) => {
// get user data from the auth trigger
const userUid = user.uid; // The UID of the user.
//const email = user.email; // The email of the user.
//const displayName = user.displayName; // The display name of the user.
// set account doc
const account = {
useruid: userUid,
calendarEvents: []
}
// write new doc to collection
return admin.firestore().collection('accounts').add(account);
});
If you are using Firebase UI to simplify your life a lil, you can add a User document to a "/users" collection in Firestore only when that user first signs up by using authResult.additionalUserInfo.isNewUser from the signInSuccessWithAuthResult in your UI config.
I'm doing something like this in my project:
let uiConfig = {
...
callbacks: {
signInSuccessWithAuthResult: (authResult) => {
// this is a new user, add them to the firestore users collection!
if (authResult.additionalUserInfo.isNewUser) {
db.collection("users")
.doc(authResult.user.uid)
.set({
displayName: authResult.user.displayName,
photoURL: authResult.user.photoURL,
createdAt: firebase.firestore.FieldValue.serverTimestamp(),
})
.then(() => {
console.log("User document successfully written!");
})
.catch((error) => {
console.error("Error writing user document: ", error);
});
}
return false;
},
},
...
}
...
ui.start("#firebaseui-auth-container", uiConfig);
The signInSuccessWithAuthResult gives you an authResult and a redirectUrl.
from the Firebase UI Web Github README:
// ...
signInSuccessWithAuthResult: function(authResult, redirectUrl) {
// If a user signed in with email link, ?showPromo=1234 can be obtained from
// window.location.href.
// ...
return false;
}

Categories