How to create new user document in firestore - javascript

I am trying to implement this logic, when user successfully registers, app creates a document with id=user.email in firestore. For that, I created following security rule in firestore:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read: if request.auth != null;
}
match /users/{userId}{
allow read: if request.auth != null;
allow write: if request.auth.token.email == userId;
}
}
}
and following code in my app:
const { email, password, name, lastName } = value;
firebase
.auth()
.createUserWithEmailAndPassword(email, password)
.then(() => {
firestore.collection('users').doc(email).set({
name, lastName
})
.then(function(docRef) {
console.log("Document written with ID: ", docRef.id);
})
.catch(function(error) {
console.error("Error adding document: ", error);
});
this.props.navigation.navigate('Main')
})
.catch(error => alert(error))
when I run my app, I am getting following error:
Error adding document: , [FirebaseError: Missing or insufficient permissions.]

It's not a good idea to use the email address of the user as a unique identifier, as that can change over time. It's better to use the unique user id (uid) assigned by Firebase Authentication.
Rules:
match /users/{userId} {
allow read: if request.auth != null;
allow write: if request.auth.uid == userId;
}
Code:
firebase
.auth()
.createUserWithEmailAndPassword(email, password)
.then(userCredential => {
firestore.collection('users').doc(userCredential.user.uid).set({
name, lastName
})
This is far more common, makes it easier to write security rules, and is resistant to email changes.

Related

Firebase Auth USER_NOT_FOUND

I am trying to create an account system that creates a new user and then sets the data of the user to Firebase Firestore.
function signUp(email, name, password, theme, privacy){
firebase.auth().createUserWithEmailAndPassword(email, password).then(function(user) {
var userId = user.uid;
firebase.firestore().collection("Users").doc(`${userId}`).set({
name: name,
theme: theme,
privacy: privacy
}).then(() => {
console.log("Document successfully written!");
}).catch((error) => {
console.error("Error writing document: ", error);
});
}).catch(function(error) {
// Handle Errors here.
var errorCode = error.code;
var errorMessage = error.message;
console.log('User did not sign up correctly');
console.log(errorCode);
console.console.log(errorMessage);
});
}
And here are my database rules
match /Users/{userId}/{documents=**} {
allow read, write: if isSignedIn() && isOwnerOfContent();
}
function isSignedIn(){
return request.auth != null;
}
function isOwnerOfContent(){
return request.auth.uid == userId;
}
Error =
{error: {code: 400, message: "USER_NOT_FOUND",…}}
error: {code: 400, message: "USER_NOT_FOUND",…}
code: 400
errors: [{message: "USER_NOT_FOUND", domain: "global", reason: "invalid"}]
0: {message: "USER_NOT_FOUND", domain: "global", reason: "invalid"}
domain: "global"
message: "USER_NOT_FOUND"
reason: "invalid"
message: "USER_NOT_FOUND
Do you see anything wrong?
I also get this error:
signUp.js:135 Error writing document: FirebaseError: Missing or insufficient permissions. at new xr (prebuilt.js:184) at prebuilt.js:10608 at pr. (prebuilt.js:10560) at Kt (eventtarget.js:351) at jt (eventtarget.js:481) at mr.sa (webchannelbasetransport.js:368) at Qe (webchannelbase.js:2219) at Ue (channelrequest.js:822) at xe.N.Ca (channelrequest.js:703) at xe.N.Xa (channelrequest.js:564)
Your isOwnerOfContent uses userId, but that variable is not in scope:
function isOwnerOfContent(){
return request.auth.uid == userId;
}
To make it work, either move isOwnerOfContent inside match /Users/{userId}/{documents=**}, or pass the userId value along in the call:
match /Users/{userId}/{documents=**} {
allow read, write: if isSignedIn() && isOwnerOfContent(userId);
}
...
function isOwnerOfContent(userId){
return request.auth.uid == userId;
}

Firebase/Firestore - How do I create a new document for a new user

When using Google SignIn I want Firestore to create a users/document for the new user matching the user.uid I have. I used the Firestore rules from this post.
Edit: However, still get this error:
C:\Users\alobre\Documents\Programmieren\BountyHunter\node_modules\react-devtools-core\dist\backend.js:32 Possible Unhandled Promise Rejection (id: 1):
Error: [firestore/permission-denied] The caller does not have permission to execute the specified operation.
NativeFirebaseError: [firestore/permission-denied] The caller does not have permission to execute the specified operation.
at FirestoreCollectionReference.get (http://localhost:8081/index.bundle?platform=android&dev=true&minify=false:133094:39)
at _callee$ (http://localhost:8081/index.bundle?platform=android&dev=true&minify=false:197066:93)
at tryCatch (http://localhost:8081/index.bundle?platform=android&dev=true&minify=false:24879:19)
at Generator.invoke [as _invoke] (http://localhost:8081/index.bundle?platform=android&dev=true&minify=false:25052:24)
at Generator.next (http://localhost:8081/index.bundle?platform=android&dev=true&minify=false:24922:23)
at tryCatch (http://localhost:8081/index.bundle?platform=android&dev=true&minify=false:24879:19)
at invoke (http://localhost:8081/index.bundle?platform=android&dev=true&minify=false:24952:22)
at http://localhost:8081/index.bundle?platform=android&dev=true&minify=false:24982:13
at tryCallTwo (http://localhost:8081/index.bundle?platform=android&dev=true&minify=false:26948:7)
Here is my code:
addUser
export default function addUser(user){
console.log(firestore())
firestore()
.collection('users')
.doc(user.uid)
.set({
user
})
.then(() => {
console.log('User added!');
});
}
called when pressing login button
async function onGoogleButtonPress() {
GoogleSignin.signIn()
.then((data) => {
const credential = auth.GoogleAuthProvider.credential(data.idToken, data.accessToken);
return auth().signInWithCredential(credential);
})
.then((user) => {
addUser(user)
})
.catch((error) => {
const { code, message } = error;
});
}
My Firestore rules:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Allow users to create a document for themselves in the users collection
match /users/{document=**} {
allow create: if request.resource.id == request.auth.uid &&
!("admin" in request.resource.data);
}
// Allow users to read, write, update documents that have the same ID as their user id
match /users/{userId} {
// Allow users to read their own profile (doc id same as user id)
allow read: if request.auth.uid == userId;
// Allow users to write / update their own profile as long as no "admin"
// field is trying to be added or created - unless they are already an admin
allow write, update: if request.auth.uid == userId &&
(
!("admin" in request.resource.data) ||
get(/databases/$(database)/documents/users/$(request.auth.uid)).data.admin == true // allow admin to update their own profile
)
}
}
}
SOLVED:
the firestore rules are correct. However i got a error that the permission is denied.
the reason:
match /users/{document=**} {
allow create: if request.resource.id == request.auth.uid &&
!("admin" in request.resource.data);
}
the request.resource.id does not match the request auth.uid
I was trying to create a document with the uid of the GoogleSignin module (which doesn't even have a uid but a id).
The correct way would be:
.then(
Post(auth().currentUser)
)
Firestore needs this auth().currentUser.uid and I was giving the GoogleSignin.signIn().uid which does not match the request.auth.uid

Firestore/Firebase permissions denied using onDisconnect()

In this Javascript, everything is working except the presenceRef.onDisconnect().set('no more'); statement, which generates PERMISSION_DENIED: Permission denied in my console.
firebase.auth().onAuthStateChanged(function(user){
if (user){
uid=user.uid;
db.collection('user').doc(uid).set({
custnum: parseInt(custnum),
email: email,
password: password,
screenname: screenname,
admin: parseInt(admin),
uid: uid
})
.then(function(){
var docpath='/user/'+uid+'/email';
var presenceRef=firebase.database().ref(docpath);
presenceRef.onDisconnect().set('no more');
})
.catch(function(error){
console.log('Error writing document: ' + error);
});
} else {
db.collection('user').doc(uid).delete({
})
.then(function(){
//success
})
.catch(function(error){
console.log('Error writing document: ' + error);
});
uid='';
}
});
In the Rules tab when I am connected to this URL:
https://console.firebase.google.com/project/[Project Name]/database/firestore/rules
...I have everything opened up, as follows:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if true;
}
}
}
I can use .set to write to the database. Why can I not use onDisconnect() to .set a different value? .db refers to a firebase.firestore() connection. Is there something going on here between Firebase and Firestore that I'm not understanding?
onDisconnect() is a method for the realtime database and not Firestore, it allows you to write or clear data when your client disconnects from the Database server.
Therefore if you meant to use it, then change the rules for the realtime database to the following:
{
"rules": {
".read": true,
".write": true
}
}
only use those rules in development.

Only authenticate with Google Auth Provider if user exists

I am using Firebase to authenticate users in our app using GoogleAuthProvider. But I don't want a new user to sign in if they are not already an authenticated user.
If the user exists then sign them in and console.log('user ' + user.email + ' does exist!');.
However, if the user does not exist. Then do not allow authentication and console.log('user ' + user.email + ' does not exist!')
var googleProvider = new firebase.auth.GoogleAuthProvider();
export const doSignInWithGoogle = () => auth.signInWithPopup(googleProvider);
googleLogin = () => {
auth
.doSignInWithGoogle()
.then(result => {
var user = result.user;
const userRef = db.collection('users').doc(user.uid);
userRef.get().then(docSnapshot => {
if (docSnapshot.exists) {
userRef.onSnapshot(() => {
console.log('user ' + user.email + ' does exist!');
});
} else {
console.log('user ' + user.email + ' does not exist!');
}
});
})
.catch(error => {
this.setState(updateByPropertyName('error', error));
});
};
I thought referencing the user records in Firestore would be a simple approach to this. However, perhaps Firebase Auth already have a way to do this. I cannot find documentation or any example.
In the above code, nothing gets logged and the user is either created or logged in.
How can I stop new users from signing up, whilst still allowing current users to sign in?
If you really want to use signInWithPopup method, you have this option,
but it's not the best way. when you are signing in with google, signInWithPopup method returns a promise. you can access the isNewUser property in additionalUserInfo from resulting object. then delete the user you just created.
firebase.auth().signInWithPopup(provider).then(
function (result) {
var token = result.credential.accessToken;
var user = result.user;
//this is what you need
var isNewUser = result.additionalUserInfo.isNewUser;
if (isNewUser) {
//delete the created user
result.user.delete();
} else {
// your sign in flow
console.log('user ' + user.email + ' does exist!');
}
}).catch(function (error) {
// Handle Errors here.
});
This is the easy way but deleting after creating is not the best practice. There is another option,
you can use, signInAndRetrieveDataWithCredential method for this. according to the docs,
auth/user-not-found will be
Thrown if signing in with a credential from
firebase.auth.EmailAuthProvider.credential and there is no user
corresponding to the given email.
function googleSignInWithCredentials(id_token) {
// Build Firebase credential with the Google ID token.
var credential = firebase.auth.GoogleAuthProvider.credential(id_token);
// Sign in with credential from the Google user.
firebase.auth().signInAndRetrieveDataWithCredential(credential)
.then(function (userCredential) {
//sign in
console.log(userCredential.additionalUserInfo.username);
}).catch(function (error) {
// Handle Errors here.
var errorCode = error.code;
if (errorCode === 'auth/user-not-found') {
//handle this
} else {
console.error(error);
}
});
}
here is an example from firebase github repo.
with Firebase security rules, can only check if keys exist - therefore searching in the users table is not an option:
"emails": {
"example1#gmail.com": true,
"example2#gmail.com": true
}
and then one can check with security rules, if the auth.token.email exists as a key:
{
"rules": {
".read": "root.child('emails').child(auth.token.email).exists(),
".write": false,
}
}
in the client, this should throw an "The read failed: Permission denied error" error then, to be handled accordingly. hooking into the Firebase sign-up isn't possible - but while they cannot log-in, this has the same effort (except that on has to clean up the user-database from time to time); eg. with a Cloud Function, which deletes users, which do not have their email as key in the emails "table".
in Firestore security rules, one can check with:
request.auth.token.email & request.auth.token.email_verified
for example, with a collection called emails and a collection called content:
match /databases/{database}/documents {
function userMatchesId(userId) {
return request.auth != null && request.auth.uid == userId
}
function readAllowed(email) {
return if get(/databases/$(database)/documents/emails/$(request.auth.token.email)).data != null
}
match /users/{userId} {
allow get: if userMatchesId(userId)
}
match /content {
allow get: if readAllowed(request.auth.token.email)
}
}
The object you receive from firebase after login has additionalUserInfo where you have the property isNewUser.
You can find the reference here: https://firebase.google.com/docs/reference/js/firebase.auth.html#.AdditionalUserInfo

User should only read / write own submits on Firestore

I am lost with the Firestore Rules.
I want authenticated users to be able to read their own submits, but I keep receiving insufficient permissions. I am writing the userId into each submit.
// add submit to submits collection in firestore
db.collection('submits').add({
user: this.user,
number: this.number,
timestamp: moment.utc(this.timestamp).format(),
usage: this.usage
})
Here I check which user is logged in and fetch the user his submits
let ref = db.collection('users')
// get current user
ref.where('user_id', '==', firebase.auth().currentUser.uid).get()
.then(snapshot => {
snapshot.forEach(doc => {
this.user = doc.data()
this.user = doc.data().user_id
})
})
.then(() => {
// fetch the user previous submits from the firestore
db.collection('submits').where('user', '==', this.user).get()
.then(snapshot => {
// console.log(snapshot)
snapshot.forEach(doc => {
let submit = doc.data()
submit.id = doc.id
submit.timestamp = moment(doc.data().timestamp).format('lll')
this.previousSubmits.push(submit)
})
})
})
}
These are my firestore rules
service cloud.firestore {
match /databases/{database}/documents {
// Make sure the uid of the requesting user matches name of the user
// document. The wildcard expression {userId} makes the userId variable
// available in rules.
match /users/{userId} {
allow read, update, delete: if request.auth.uid == userId;
allow create: if request.auth.uid != null;
}
// check if the user is owner of submits he is requesting
match /submits/{document=**} {
allow read: if resource.data.user == request.auth.uid;
allow write: if request.auth.uid != null;
}
}
}
Does anybody knows what I'm doing wrong?
Update, added the code that I use to create the user document in the users collection:
signup () {
if (this.alias && this.email && this.password) {
this.slug = slugify(this.alias, {
replacement: '-',
remove: /[$*_+~.()'"!\-:#]/g,
lower: true
})
let ref = db.collection('users').doc(this.slug)
ref.get().then(doc => {
if (doc.exists) {
this.feedback = 'This alias already exists'
} else {
firebase.auth().createUserWithEmailAndPassword(this.email, this.password)
.then(cred => {
ref.set({
alias: this.alias,
household: this.household,
user_id: cred.user.uid
})
}).then(() => {
this.$router.push({ name: 'Dashboard' })
})
.catch(err => {
console.log(err)
this.feedback = err.message
})
this.feedback = 'This alias is free to use'
}
})
}
}
Part of the problem is that you're trying to search for a document where the user_id field is equal to the user's ID, but your security rules are saying, "Only let users read a document if the ID of the document is the same as the user's ID", which is completely unrelated, and I don't know if that's actually true in your case.
One option is to change your rules where you say, "Hey, you can read / change a document if the value of the user_id field is equal to your user ID, which would be something like this...
match /users/{userId} {
allow read, update, delete: if request.auth.uid == resource.data.user_id;
// ...
}
...or to change your query so that you're querying for the specific document whose ID is the UID of the current user.
let ref = db.collection('users').document(currentUser.uid)
ref.get() {... }
But, I guess, don't do both at the same time. :)
The error is coming from the first line where you try to get the current user:
ref.where('user_id', '==', firebase.auth().currentUser.uid).get()
This line is selecting documents where the user_id field matches the current user's ID, but your Firestore rule is checking to see if the user's ID matches the ID of the document. Since you're generating the document ID with slugify, it doesn't match, which is causing the permissions error.
Todd's suggestion for the rule change will work, but it would probably be best to just use the user ID as the document ID and store the user's slug as a field. That way you don't have to worry about collisions (user IDs will automatically be unique) and don't have to use a select statement -- you will know there is only one document per user and can just access it directly:
ref.doc(firebase.auth().currentUser.uid).get()
As a side note, you don't actually seem to have any reason to retrieve the user document at that point (at least in your example code). If you're really only using that to get the user_id field, you can just skip it since you already have the user ID from the auth session. In that case you should just do this:
this.user = firebase.auth().currentUser.uid
db.collection('submits').where('user', '==', this.user).get()
.then(snapshot => {
// console.log(snapshot)
snapshot.forEach(doc => {
let submit = doc.data()
submit.id = doc.id
submit.timestamp = moment(doc.data().timestamp).format('lll')
this.previousSubmits.push(submit)
})
})

Categories