This question already has answers here:
Cloud Firestore: Enforcing Unique User Names
(8 answers)
Closed 3 years ago.
What i'am attempting to do is create new users with Firebase authentication and create their own document entry within Firestore. So far everything went well until i wanted to create unique usersnames. What is the most optimal way of going about doing this? should i create an array of all the users to cross-reference upon each sign up, or perhaps create a collection dedicated to Usernames with their email attached (which you can see i did in the second call to Firebase)
Firebase auth takes in email and passwords only so thats out of the question, or at least from what i gather.
export const signInUser = async ({ name, email, password }) => {
try {
await firebase
.auth()
.createUserWithEmailAndPassword(email, password)
.then(cred => {
return firebase
.firestore()
.collection("users")
.doc(cred.user.uid)
.set({
name: name,
email: email,
friends: []
});
});
await firebase
.firestore()
.collection("usernames")
.doc(name)
.set({ username: name, email: email });
firebase.auth().currentUser.updateProfile({
displayName: name
});
return {};
} catch (error) {
switch (error.code) {
case "auth/email-already-in-use":
return {
error: "E-mail already in use."
};
case "auth/invalid-email":
return {
error: "Invalid e-mail address format."
};
case "auth/weak-password":
return {
error: "Password is too weak."
};
case "auth/too-many-requests":
return {
error: "Too many request. Try again in a minute."
};
default:
return {
error: "Check your internet connection."
};
}
}
};
I'd skip the "usernames" collection here, as you're basically creating an index, which fireStore already does for you. To check whether a name is unique, you can do:
const snapshot = await firestore.collection("users").where("name", "==", name).get();
// The name is taken if snapshot.empty is false.
Related
This question already has answers here:
Firestore- checking if username already exists [duplicate]
(1 answer)
Firestore unique index or unique constraint?
(5 answers)
Check a document field for a specific value in Cloud Firestore
(3 answers)
Closed 1 year ago.
I need a littel help with Firestore. I have a users data collection, user id-documents with each user information. I want to check if username already exists; // "That username already exists". How do I do it?
const signUpForm = document.querySelector('#signup-form');
signUpForm.addEventListener('submit', (evt) =>{
evt.preventDefault();
//get user email, password
const email= signUpForm['signup-email'].value;
const password = signUpForm['signup-password'].value;
const repassword = signUpForm['signup-re-password'].value;
if(password != repassword)
{
alert("Passwords don't match \nPlease try again");
}
else{
if(//check username exist?)
{
alert('This username already exist !');
}
else { //sign up the user
auth.createUserWithEmailAndPassword(email, password)
.then(cred => {
return db.collection('users').doc(cred.user.uid).set({
neptun: signUpForm['signup-neptun'].value,
nickName: signUpForm['signup-nickName'].value
});
});
}
});
Update!
A working solution :
const username = signUpForm['signup-username'].value;
db.collection("users").where("username", "==", username).get().then((doc) => {
if(!doc.empty) {
alert("This username is already taken!");
}
else{
//sign up the user
});
To be straight forward you can just try this code:
const username = ""
const userNameDoc = await firebase.firestore().collection("users").where("username", "==", username).get()
if(!userNameDoc.empty) {
console.log("This username is already taken!");
}
But as this is all frontend so this can be bypassed if anyone wants to. All this requires you to give all users access to the whole users collection which won't be ideal. So you should ideally use a cloud function or you own server environment for better security.
For example, you can block all direct requests to Firestore collection using security rules and create users using the Admin SDK in cloud functions.
You can use the code below in your cloud function to create a new user by checking if the username is still valid.
exports.createNewUser = functions.https.onCall(async (data, context) => {
const userEmail = data.email
const userName = data.name
const userPass = data.password
const userNameDoc = await admin.firestore().collection("users").where("username", "==", username).get()
if(!userNameDoc.empty) {
console.log("This username is already taken!");
return {result: "Username is already taken!"}
}
return admin
.auth()
.createUser({
email: 'user#example.com',
password: userPassword,
displayName: userName,
})
.then((userRecord) => {
// See the UserRecord reference doc for the contents of userRecord.
console.log('Successfully created new user:', userRecord.uid);
return {result: "User Created!"}
})
.catch((error) => {
console.log('Error creating new user:', error);
return {result: error}
});
});
The 2nd method is way more secure than first one for your Firestore DB.
Firebase automatically checks if an email is already in use.
The createUserWithEmailAndPassword function throws an error, when a user tries to sign up with an email address already in use.
Catch errors and then check for the "auth/email-already-in-use" error code:
.catch(function(error){
if(error.code=="auth/email-already-in-use"){//do something}
})
For more details, see the firebase auth reference https://firebase.google.com/docs/reference/js/firebase.auth.Auth?authuser=1#createuserwithemailandpassword
I've created a simple createUser function which is executed on call. I have one problem though. The function is crashing when the user is trying to register with an already existing email. I mean, it's ok, since no one wants to have 2 users with the same email address but I want to prevent crushing function, instead, I want to send an error message as a response.
export const createUserTest = functions.https.onCall((data, context) => {
const {email, password} = data;
return new Promise((resolve, reject)=>{
try{
admin
.auth()
.createUser({
email: email,
emailVerified: false,
password: password,
disabled: false,
})
.then((user) => {
resolve({
result: 'success',
user: user,
}) ;
})
.catch((error) => {
reject(error) ;
});
}catch(error) {
reject (error)
}
})
});
I tried to put the function in to try/catch block but it didn't help. Do you have an idea of how I can achieve my goal?
As explained in the doc for Callable Cloud Functions, "to ensure the client gets useful error details, return errors from a callable by throwing (or returning a Promise rejected with) an instance of functions.https.HttpsError".
The error has a code attribute that can be one of the values listed here. In your case, the most appropriate seems to be already-exists.
On, the other hand, you'll find here the Admin SDK Authentication errors list and you'll see that in case the provided email is already in use by an existing user the error code is auth/email-already-exists.
So you can adapt your code as follows:
export const createUserTest = functions.https.onCall((data, context) => {
const { email, password } = data;
return admin
.auth()
.createUser({
email: email,
emailVerified: false,
password: password,
disabled: false,
})
.then((user) => {
return {
result: 'success',
user: user,
}
})
.catch((error) => {
if (error.code === 'auth/email-already-exists') {
throw new functions.https.HttpsError('already-exists', 'The provided email is already in use by an existing user');
} else {
throw new functions.https.HttpsError('...other code....', '...');
// If an error other than HttpsError is thrown, your client instead receives an error with the message INTERNAL and the code internal.
}
});
});
See here in the doc, how to handle errors on the client side. If error.code == 'already-exists' you know that it's because the email is already in use.
I'm using Firestore with some private documents (no write). My rules are already setup for this. For example, a document could contain the credits or subscription tier for a user. I want to let the backend update these fields instead of the client, for obvious reasons. However, I was wondering, if I create a generic updatePrivateField method in Cloud functions, would it be considered best practice?
exports.updateProtectedField = functions.https.onCall(async (data, context) => {
if (!context.auth) {
throw new functions.https.HttpsError(
"failed-precondition",
"Authentication Required"
);
}
const { collection, id, update } = data;
try {
await admin
.firestore()
.collection(collection)
.doc(id)
.update({
...update,
});
return { msg: "Update successful", code: 200 };
} catch (error) {
throw new functions.https.HttpsError("unknown", error.message, error);
}
});
Basically, what I am wondering is, is creating an endpoint like this considered safe? I am checking if the user is authenticated, but couldn't they just POST to the endpoint with their own login credentials and update any field in the database?
Thanks, I appreciate any help!
In case of a user can update own document.
Should set context.auth.uid as document id.
exports.updateProtectedField = functions.https.onCall(async (data, context) => {
// Check context.auth.uid
if (!context.auth || !context.auth.uid) {
throw new functions.https.HttpsError(
"failed-precondition",
"Authentication Required"
);
}
const { collection, update } = data;
// Set context.auth.uid as document id
try {
await admin
.firestore()
.collection(collection)
.doc(context.auth.uid)
.update({
...update,
});
return { msg: "Update successful", code: 200 };
} catch (error) {
throw new functions.https.HttpsError("unknown", error.message, error);
}
});
In case of a some role (ex. admin) can update a user document.
Should use Custom Claims and check it.
See https://firebase.google.com/docs/auth/admin/custom-claims
ex. Use a admin role
// Add any trigger or any condition you want.
// Set admin privilege on the user corresponding to uid.
admin.auth().setCustomUserClaims(uid, {admin: true}).then(() => {
// The new custom claims will propagate to the user's ID token the
// next time a new one is issued.
});
exports.updateProtectedField = functions.https.onCall(async (data, context) => {
// Check user is admin
if (!context.auth || !context.auth.token.admin) {
throw new functions.https.HttpsError(
"failed-precondition",
"Authentication Required"
);
}
const { collection, id, update } = data;
try {
await admin
.firestore()
.collection(collection)
.doc(id)
.update({
...update,
});
return { msg: "Update successful", code: 200 };
} catch (error) {
throw new functions.https.HttpsError("unknown", error.message, error);
}
More documents
https://firebase.google.com/docs/reference/functions/providers_https_.callablecontext#auth
https://firebase.google.com/docs/reference/admin/node/admin.auth.DecodedIdToken
https://cloud.google.com/functions/docs/concepts/functions-and-firebase
https://firebase.google.com/docs/auth/admin/verify-id-tokens#verify_id_tokens_using_the_firebase_admin_sdk
https://cloud.google.com/endpoints/docs/openapi/authenticating-users-firebase
Your Cloud Functions code allows any authenticated user to update any document. It is pretty much the equivalent of these security rules:
service cloud.firestore {
match /databases/{database}/documents {
match /{collection/{document=**} {
allow write: if request.auth != null;
}
}
}
If that is what you want to accomplish, I recommend doing so with the above security rules as it'll be both simpler and cheaper than introducing Cloud Functions to the mix.
If a user should only be able to update their own document through this (as zhoki's answer suggests by using context.auth.uid), then that'd be the equivalent of these security rules:
service cloud.firestore {
match /databases/{database}/documents {
match /{collection}/{userId} {
allow write: if request.auth != null && request.auth.uid == userId;
}
}
}
If this is the use-case you're looking for, I'd again recommend using security rules to secure it and bypass Cloud Functions for a simpler and cheaper solution.
In both of the cases above the {collection} allows the user to update documents in any collection, since that is what your Cloud Functions code also seems to do. It is much more common to limit the update to a specific collection, in which case you'd replace {collection} with just that collection name.
I am building an app that has both merchants and clients. Merchants offer their services and clients can book services from the merchants.
They BOTH are authenticated with Firebase and are on the Authentication list you can find on the Firebase Console.
On sign up, merchants' info go to a collection called 'businesses'. Clients go on a collection called 'users'.
This is how I create a 'user' document
async createUserProfileDocument(user, additionalData) {
if (!user) return
const userRef = this.firestore.doc(`users/${user.uid}`)
const snapshot = await userRef.get()
if (!snapshot.exists) {
const { displayName, email, photoURL, providerData } = user
const createdAt = moment().format('MMMM Do YYYY, h:mm:ss a')
try {
await userRef.set({
displayName,
email,
photoURL,
createdAt,
providerData: providerData[0].providerId, //provider: 'google.com', 'password'
...additionalData,
})
} catch (error) {
console.error('error creating user: ', error)
}
}
return this.getUserDocument(user.uid)
}
async getUserDocument(uid) {
if (!uid) return null
try {
const userDocument = await this.firestore.collection('users').doc(uid).get()
return { uid, ...userDocument.data() }
} catch (error) {
console.error('error getting user document: ', error)
}
}
This is how 'users' sign up
export const Register = () => {
const history = useHistory()
async function writeToFirebase(email, password, values) { //function is called below
try {
const { user } = await firebaseService.auth.createUserWithEmailAndPassword(email, password)
firebaseService.createUserProfileDocument(user, values)
} catch (error) {
console.error('error: ', error)
}
}
//Formik's onSubmit to submit a form
function onSubmit(values, { setSubmitting }) {
values.displayName = values.user.name
writeToFirebase(values.user.email, values.user.password, values) //function call
}
This is how a 'merchant' registers. They sign up with email + password and their info from a form go to a collection called 'businesses'
firebaseService.auth.createUserWithEmailAndPassword(values.user.email, values.user.password)
await firebaseService.firestore.collection('businesses').add(values) //values from a form
Here is where I would like to be able to differentiate between 'users' and 'merchants', so that I can write some logic with the 'merchant' data. so far it only works with 'users'
useEffect(() => {
firebaseService.auth.onAuthStateChanged(async function (userAuth) {
if (userAuth) {
//**how can I find out if this userAuth is a 'merchant' (business) or 'user' (client)
const user = await firebaseService.createUserProfileDocument(userAuth)
setUsername(user.displayName)
//if (userAuth IS A MERCHANT) setUserIsMerchant(true) **what I'd like to be able to do
} else {
console.log('no one signed in')
}
})
}, [])
The recommended way for implementing a role-based access control system is to use Custom Claims.
You will combine Custom Claims (and Firebase Authentication) together with Firebase Security Rules. As explained in the doc referred to above:
The Firebase Admin SDK supports defining custom attributes on user
accounts. This provides the ability to implement various access
control strategies, including role-based access control, in Firebase
apps. These custom attributes can give users different levels of
access (roles), which are enforced in an application's security rules.
Once you'll have assigned to your users a Custom Claim corresponding to their user role (e.g. a merchant or client Claim), you will be able to:
Adapt your Security Rules according to the claims;
Get the Claim in your front-end and act accordingly (e.g. route to specific app screens/pages, display specific UI elements, etc...)
More precisely, as explained in the doc, you could do something like:
useEffect(() => {
firebaseService.auth.onAuthStateChanged(userAuth => {
if (userAuth) {
userAuth.getIdTokenResult()
.then((idTokenResult) => {
// Confirm the user is a Merchant or a Client
if (!!idTokenResult.claims.merchant) {
// Do what needs to be done for merchants
} else if (!!idTokenResult.claims.client) {
// Do what needs to be done for clients
}
} else {
console.log('no one signed in')
}
})
}, [])
You may be interested by this article which presents "How to create an Admin module for managing users access and roles" (disclaimer, I'm the author).
I'm trying to create a data entry on the firebase database to store additional information about a user when they register on my site.
I've tried to write data to the database in the .then() function following createUserWithEmailAndPassword() as that's the only way for me to extract the user id for the user (I'm hoping to use the uid as the key field of the record I create)
(req, res) => {
// extract user data from the form
const newUser = {
fname: req.body.fname,
lname: req.body.lname,
email: req.body.email,
pw: req.body.pw,
pw_c: req.body.pw_c
}
// carry out validation
const { valid, errors } = validateRegistrationData(newUser);
if (!valid) return res.status(400).json(errors);
// create new firebase user
firebase
.auth()
.createUserWithEmailAndPassword(newUser.email, newUser.pw)
.then(data => {
let uid = data.user.uid;
// make a database entry to store the users info
// by default, assumes that the user is a secondary user
let userData = {
fname: newUser.fname,
lname: newUser.lname,
email: newUser.email,
utype: 1,
createdon: admin.firestore.FieldValue.serverTimestamp(),
intitems: []
}
newUserDoc = db
.collection("users")
.doc(uid)
.set(userData)
return res.status(200).json("Success: new user created.");
})
.catch(err => {
if (err.code === "auth/email-already-in-use"){
return res.status(400).json({ email: "Email is already in use" });
} else {
return res.status(500).json({ error: err.code });
}
});
return res.status(200).json("Success: new user created.");
}
The server responds with {Success: new user created."}. The authentication part seems to work as a new user is created in the Authentication section of my firebase console. However, no new data entries appear in the users collection of my database.
.set returns a promise that still needs to run to completion. However, currently you're not waiting on the promise, and instead just responding via res.send.
You can append .then(() => { do stuff here }) to the end of .set. If it's the last thing you're doing in that function, you can just do res.send from there.
return db.collection("users").doc(uid).set(userData).then(() => {
return res.status(200).json("Success: new user created.");
})
.catch(error => {
console.log(error)
})