I'm making an app with React js and firebase. When signing Up, firebase create the user and makes the alert I want but the data I want to save in firestore is not being saved. If I use the hendleSighUp function without the checkUser, it works. What I'm doing wrong? Thank you, sorry to bother you.
const checkUser = async (avatar) => {
var docRef = fb.firestore().collection("links").doc(user2);
var doc = await docRef.get();
if (doc.exists) {
//invalidUsername(true);
console.log("error");
} else {
handleSignUp();
console.log("Working");
//history.push("/");
}
};
const handleSignUp = () => {
clearErrors();
fb.auth()
.createUserWithEmailAndPassword(email, password)
.then((userCredential) => {
// send verification mail.
userCredential.user.sendEmailVerification();
handleLogOut();
fb.firestore()
.collection("links")
.doc("Andres")
.set({ name: "Andres" });
alert("Email sent");
})
.catch((err) => {
switch (err.code) {
case "auth/email-already-in-use":
case "auth/invalid-email":
setEmailError(err.message);
break;
case "auth/weak-password":
setPasswordError("Contrasena debil");
break;
}
});
};
I would make the function async and use await while setting the document to make sure the promise is resolved and also as alert is synchronous do it'll will block any code execution until its dismissed. Because you are not using await the alert will trigger right away.
fb.auth()
.createUserWithEmailAndPassword(email, password)
.then(async (userCredential) => {
// ^^^^^
// send verification mail.
userCredential.user.sendEmailVerification();
handleLogOut();
await fb.firestore()
^^^^^
.collection("links")
.doc("Andres")
.set({ name: "Andres" });
alert("Email sent");
})
Related
I'm trying to use Firebase custom claims to protect content for my users, but the first time a user signs up and is redirected to /protectedpage, they cannot view the page because their claim is not set. If they log out and log back in, everything works properly.
Signup Flow
User signs up with email and password
A user document is created in a users collection in Firestore
The user is redirected to /protectedpage
Creation of the user document triggers a cloud function which assigns the custom claim role=A or role=B depending on the information in the user document.
In Javascript (React), it looks like this
Client side
// Create a new user with email and password
createUserWithEmailAndPassword(auth, formValues.email, formValues.password)
.then((userCredential) => {
// Signed in
const user = userCredential.user;
// Add a new document in collection "users"
setDoc(doc(db, "users", user.uid), {
account_type: formValues.account_type,
full_name: formValues.full_name,
});
// Send email verification
sendEmailVerification(userCredential.user)
.then(() => {
// Redirect to home page
router.push('/protectedpage');
})
.catch((error) => {
console.log("Error sending email verification", error.message);
});
})
.catch((error) => {
setFormError(error.message);
})
Server side
const functions = require('firebase-functions')
const { initializeApp } = require('firebase-admin/app');
const { getAuth } = require('firebase-admin/auth');
initializeApp();
// This function runs when a document is created in
// the users collection
exports.createUser = functions.firestore
.document('users/{userId}')
.onCreate(async (snap, context) => {
// Get an object representing the document
const doc = snap.data()
const userId = context.params.userId;
// Declare customClaims
let customClaims = {};
// Assign user role
if (doc.account_type == 'A') {
customClaims["role"] = "A"
} else if (doc.account_type == 'B') {
customClaims["role"] = "B"
} else {
functions.logger.info('A role could not be assigned to user:', doc)
response.send('Error: A role could not be assigned')
}
try {
// Set custom user claims on this newly created user.
await getAuth().setCustomUserClaims(userId, customClaims);
} catch (error) {
functions.logger.info(error);
}
return "OK"
})
By the time the user gets to /protectedpage, his JWT does not have the custom claim.
Authorization
My authorization code is using a React context manager, and looks like this
import { createContext, useContext, useEffect, useState } from 'react'
import { onAuthStateChanged, signOut as authSignOut } from 'firebase/auth'
import { auth } from './firebase'
export default function useFirebaseAuth() {
const [authUser, setAuthUser] = useState(null)
const [isLoading, setIsLoading] = useState(true)
const clear = () => {
setAuthUser(null)
setIsLoading(false)
}
const authStateChanged = async (user) => {
setIsLoading(true)
if (!user) {
clear()
return
}
// Use getIdTokenResult() to fetch the custom claims
user.getIdTokenResult()
.then((idTokenResult) => {
console.log("idTokenResult", idTokenResult)
setAuthUser({
uid: user.uid,
email: user.email,
role: idTokenResult.claims.role,
})
setIsLoading(false)
})
.catch((error) => {
console.log(error)
})
}
const signOut = () => authSignOut(auth).then(clear)
// Listen for Firebase Auth state change
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, authStateChanged)
return () => unsubscribe()
}, [])
return {
authUser,
isLoading,
signOut,
}
}
const AuthUserContext = createContext({
authUser: null,
isLoading: true,
signOut: async () => {},
})
export function AuthUserProvider({ children }) {
const auth = useFirebaseAuth()
return (
<AuthUserContext.Provider value={auth}>{children}</AuthUserContext.Provider>
)
}
export const useAuth = () => useContext(AuthUserContext)
If I change user.getIdTokenResult() to user.getIdTokenResult(true), the user no longer has to sign out and sign back in to access the custom claim BUT
They need to manually refresh the page to acquire the custom claim
I think this is bad, as it's going to forcibly refresh the token on every page load ??
The Firebase docs seem to address this problem with some trickery involving "metadataRef" but I don't understand it exactly, as I think it's related to the Realtime database whereas I'm using Firestore.
Finally got this to work. Two things were tripping me up.
router.push('/protectedpage') doesn't do a hard refresh. I changed this to window.location.replace('/protectedpage')
Instead of assigning the custom claim on creation of the user record, I wrote a cloud function to do it. After my user is created, I call this function. After I get the response, then I redirect the user to /protectedpage
My cloud function looks like this
const functions = require('firebase-functions')
const { initializeApp } = require('firebase-admin/app');
const { getAuth } = require('firebase-admin/auth');
initializeApp();
// IMPORTANT:
// Note the distinction between onCall and onRequest
// With onCall, authentication / user information is automatically added to the request.
// https://stackoverflow.com/questions/51066434/firebase-cloud-functions-difference-between-onrequest-and-oncall
// https://firebase.google.com/docs/functions/callable
// Function to set a user's role as either "A" or "B"
exports.setRole = functions.https.onCall((data, context) => {
// Check that the user is authenticated.
if (!context.auth) {
// Throw an HttpsError so that the client gets the error details.
// List of error codes: https://firebase.google.com/docs/reference/node/firebase.functions#functionserrorcode
throw new functions.https.HttpsError(
'failed-precondition',
'The function must be called while authenticated.'
);
}
// Confirm that the function contains a role
if (!data.hasOwnProperty("role")) {
throw new functions.https.HttpsError(
'failed-precondition',
"The function data must contain a 'role'"
);
}
// Confirm that role is either A or B
if (data.role !== "A" && data.role !== "B") {
throw new functions.https.HttpsError(
'failed-precondition',
"'role' must be set to either 'A' or 'B'"
);
}
// Confirm that the user doesn't already have a role
if (context.auth.token.role) {
throw new functions.https.HttpsError(
'failed-precondition',
"The user's role has already been set"
);
}
// Assign the role
// IMPORTANT:
// We need to return the promise! The promise returns the response. This way, on the client,
// we can wait for the promise to get resolved before moving onto the next step.
return getAuth().setCustomUserClaims(context.auth.uid, { role: data.role })
.then(() => {
return "OK"
})
.catch((error) => {
throw new functions.https.HttpsError(
'internal',
'Error setting custom user claim'
);
})
})
and I call it from the client like this
// Handle form submission
const onSubmit = (formValues) => {
// Create a new user with email and password
createUserWithEmailAndPassword(auth, formValues.email, formValues.password)
.then((userCredential) => {
// Signed in
const user = userCredential.user;
// Send email verification
sendEmailVerification(user);
// Add a new document in collection "users"
const promise1 = setDoc(doc(db, "users", user.uid), {
account_type: formValues.account_type,
full_name: formValues.full_name,
});
// Set the user role (custom claim)
// Then force refresh the user token (JWT)
const setRole = httpsCallable(functions, 'setRole');
const promise2 = setRole({ role: formValues.account_type })
.then(() => user.getIdTokenResult(true));
// When the user document has been created and the role has been set,
// redirect the user
// IMPORTANT: router.push() doesn't work for this!
Promise.all([promise1, promise2]).then((values) => {
window.location.replace('/protectedpage');
})
})
.catch((error) => {
setFormError(error.message);
})
}
Node.js CODE
exports.user = async (req, res) => {
try {
const { wallet } = req.body;
if (!wallet) {
res.status(400).json({ error: "Not logged in" });
return;
} else {
user = User.findone(wallet);
// if user is not found then create a new user and mark as loggged In
if (!user) {
User.create({
user: wallet,
});
}
// if user found then create a session token and mark as logged
in
res.send({
user: wallet,
});
}
} catch (error) {
console.log(`ERROR::`, error);
}
};
REACTJs CODE
// post call/update
const axiosCall = async () => {
// core login will give a unique username by fulling a transcation
// core.login i dont have any control
const userAccount = await core.login();
try {
const res = await Axios.post(`${API}/user`, userAccount, dataToken);
setData({
...data,
error: "",
success: res.data.message,
});
} catch (error) {
setData({
...data,
error: error.response.data.error,
});
}
};
Now here the problem occurs when some one could modify userAccount in the front-end or someone could send a body with wallet: anything to my route localhost:3000/api/user
There is no option for me to check if some actually used core.login(); to get the wallet address.
So is there any solution?
I was thinking to allow only my server IP or localhost to hit the route localhost:3000/api/user and is that even possible?
Also there is another issue anyone could modify userAccount in front-end.
1.I'm working on an backend API but at some point I need to get user data from another API. I am trying to use Axios to make http request in order to do that. The request return the result in the browser as expected but the problem is that I can't display console log in the terminal. It doesn't show anything even though I asked the program to do so. Is there a problem probably with my code?
2.Error message =>>> POST http://localhost:8000/api/register 400 (Bad Request) Error: Request failed with status code 400`
const handleSubmit = async () => {
//e.preventDefault();
try
{
// console.log(name, email, password, secret);
const { data } = await axios.post("http://localhost:8000/api/register", {
name,
email,
password,
secret,
});
setOk(data.ok); //useState component
}
catch (error) {
**strong text**
console.log(error.response.data);
}
}
import User from '../models/user'
//import{ hashPassword, comparePassword } from '../helpers/auth'
export const register = async (req,res) => {
//console.log('Register endpoint =>', req.body)
//to make this work make express.json is applied in the above middleware
//console.log error to debug code
const {name, email, password, secret} = req.body;
//validation
if(!name) return res.status(400).send('Name is required')
if(!password || password.length < 6) return res.status(400).send('Password is
short
or password is not entered')
if(!secret) return res.status(400).send('Answer is required')
//The above code is for validation purpose to make sure data is correctly
entered
const exist = await User.findOne({email })
if(exist) return res.status(400).send('Email is taken')
}
.catch(error => {
console.log(error)
})
May be catching error on your axios is wrong try this
I wrote a Register component in react, it is a simple form that on submit will post to an API. The call to the API will return an object with certain data, this data will be then added to the redux store.
I wrote some tests for this. I'm using Mock Service Worker (MSW) to mock the API call. This is my first time for writing these kind of tests so I'm not sure if I'm doing anything wrong, but my understanding was that MSW would intercept the call to the API and return whatever I specify in the MSW config, after that it should follow the regular flow.
Here's my reducer:
const authReducer = (state = INITIAL_STATE, action) => {
switch (action.type) {
case actionTypes.REGISTER_NEW_USER:
const newUser = new User().register(
action.payload.email,
action.payload.firstName,
action.payload.lastName,
action.payload.password
)
console.log("User registered data back:");
console.log(newUser);
return {
...state,
'user': newUser
}
default:
return state;
}
}
this is my User class where the actual call is performed:
import axios from "axios";
import { REGISTER_API_ENDPOINT } from "../../api";
export default class User {
/**
* Creates a new user in the system
*
* #param {string} email - user's email address
* #param {string} firstName - user's first name
* #param {string} lastName - user's last name
* #param {string} password - user's email address
*/
register(email, firstName, lastName, password) {
// console.log("registering...")
axios.post(REGISTER_API_ENDPOINT, {
email,
firstName,
lastName,
password
})
.then(function (response) {
return {
'email': response.data.email,
'token': response.data.token,
'active': response.data.active,
'loggedIn': response.data.loggedIn,
}
})
.catch(function (error) {
console.log('error');
console.log(error);
});
}
}
this is my action creator:
export function createNewUser(userData) {
return {
type: REGISTER_NEW_USER,
payload: userData
}
}
this is the onSubmit method in my Register component:
const onSubmit = data => {
// console.log(data);
if (data.password !== data.confirmPassword) {
console.log("Invalid password")
setError('password', {
type: "password",
message: "Passwords don't match"
})
return;
}
// if we got up to this point we don't need to submit the password confirmation
// todo but we might wanna pass it all the way through to the backend TBD
delete data.confirmPassword
dispatch(createNewUser(data))
}
and this is my actual test:
describe('Register page functionality', () => {
const server = setupServer(
rest.post(REGISTER_API_ENDPOINT, (req, res, ctx) => {
console.log("HERE in mock server call")
// Respond with a mocked user object
return res(
ctx.status(200),
ctx.json({
'email': faker.internet.email(),
'token': faker.datatype.uuid(),
'active': true,
'loggedIn': true,
}))
})
)
// Enable API mocking before tests
beforeEach(() => server.listen());
// Reset any runtime request handlers we may add during the tests.
afterEach(() => server.resetHandlers())
// Disable API mocking after the tests are done.
afterAll(() => server.close())
it('should perform an api call for successful registration', async () => {
// generate random data to be used in the form
const email = faker.internet.email();
const firstName = faker.name.firstName();
const lastName = faker.name.lastName();
const password = faker.internet.password();
// Render the form
const { store } = renderWithRedux(<Register />);
// Add values to the required input fields
const emailInput = screen.getByTestId('email-input')
userEvent.type(emailInput, email);
const firstNameInput = screen.getByTestId('first-name-input');
userEvent.type(firstNameInput, firstName);
const lastNameInput = screen.getByTestId('last-name-input');
userEvent.type(lastNameInput, lastName);
const passwordInput = screen.getByTestId('password-input');
userEvent.type(passwordInput, password);
const confirmPasswordInput = screen.getByTestId('confirm-password-input');
userEvent.type(confirmPasswordInput, password);
// Click on the Submit button
await act(async () => {
userEvent.click(screen.getByTestId('register-submit-button'));
// verify the store was populated
console.log(await store.getState())
});
});
So I was expecting my call to be intercepted whenever the REGISTER_API_ENDPOINT url is detected, and the value of the mocked call to be added to my redux state instead of the value of the actual API call in register method but that doesn't seem to be happening. If that's not the way to test a value in the store, how else can I achieve that?
So at the end of my test, when printing the store I was expecting to see:
{ auth: { user:
{
'email': faker.internet.email(),
'token': faker.datatype.uuid(),
'active': true,
'loggedIn': true,
}
}
but instead I'm seeing:
{ auth: { user: null } }
Is this the right approach for this test?
Thanks
EDIT
Doing some refactoring based on the comments. Now my onSubmit method looks like:
const onSubmit = async data => {
if (data.password !== data.confirmPassword) {
console.log("Invalid password")
setError('password', {
type: "password",
message: "Passwords don't match"
})
return;
}
// if we got up to this point we don't need to submit the password confirmation
// todo but we might wanna pass it all the way through to the backend TBD
delete data.confirmPassword
let user = new User()
await user.register(data).
then(
data => {
// console.log("Response:")
// console.log(data)
// create cookies
cookie.set("user", data.email);
cookie.set("token", data.token);
dispatch(createNewUser(data))
}
).catch(err => console.log(err))
Notice that now I'm dispatching the response from User.register in here instead of doing it in User.register. Also notice that this function is now async and await for the register function call to be finalized, at that moment it'll populate the store.
The register method now looks like the following:
async register(data) {
let res = await axios.post(REGISTER_API_ENDPOINT, {
'email': data.email,
'firstName': data.firstName,
'lastName': data.lastName,
'password': data.password
})
.then(function (response) {
return response
})
.catch(function (error) {
console.log('error');
console.log(error);
});
return await res.data;
}
now it's only in charge of performing the API call and returning the response.
The reducer was also simplified not to have any side effect changes, so it looks like:
const authReducer = (state = INITIAL_STATE, action) => {
switch (action.type) {
case actionTypes.REGISTER_NEW_USER:
const newUser = action.payload
return {
...state,
'user': newUser
}
default:
return state;
}
}
my test is mostly the same, the only difference is the part where I'm inspecting the store value:
// Click on the Submit button
await act(async () => {
userEvent.click(screen.getByTestId('register-submit-button'));
});
await waitFor(() => {
// verify the store was populated
console.log("Store:")
console.log(store.getState())
})
Now, this sometimes work and sometimes does not. Meaning, sometimes I get correct store printed as follows:
console.log
Store:
at test/pages/Register.test.js:219:21
console.log
{
auth: {
user: {
email: 'Selena.Tremblay#hotmail.com',
token: '1a0fadc7-7c13-433b-b86d-368b4e2311eb',
active: true,
loggedIn: true
}
}
}
at test/pages/Register.test.js:220:21
but sometimes I'm getting null:
console.log
Store:
at test/pages/Register.test.js:219:21
console.log
{ auth: { user: null } }
at test/pages/Register.test.js:220:21
I guess I'm missing some async code somewhere but I cannot put a pin on where is it.
There are some Redux rules that are being broken here:
Don't do side effects in reducers:
reducers should be pure functions: for the same input, return always
the same output. This is not the place to do API calls.
State should be immutable: you should never change a state value by reference, always provide a new state with a new object containing the changes.
So, the classical redux approach would be to have three actions in Redux: REGISTER_USER, REGISTER_USER_SUCCEEDED, REGISTER_USER_FAILED .
reducer:
const authReducer = (state = INITIAL_STATE, action) => {
switch (action.type) {
case actionTypes.REGISTER_USER:
return {
...state,
status: 'loading'
}
case actionTypes.REGISTER_USER_SUCCEEDED:
return {
...state,
status: 'idle',
user: action.user
}
case actionTypes.REGISTER_USER_FAILED:
return {
...state,
status: 'error'
}
default:
return state;
}
}
Then, async work should be done in your event handlers:
onSubmit:
const onSubmit = async data => {
// ...
dispatch(registerNewUser());
const user = new User()
try {
await user.register(data);
dispatch(registerNewUserSucceeded(user));
} catch(e) {
console.error(e);
dispatch(registerNewUserFailed());
}
}
**Don't forget to return the promise from axios inside your register function, so you can await on the promise. Currently, you are only calling axios, but not updating or returning anything...
What's great about this, is that testing your store doesn't require you to do any network calls! You could ditch MSW (although it's a great lib, just not needed here).
In your tests, just check your store state before and after every transition:
const mockUser = {...} // provide a mock user for your test
const store = createStore(authReducer);
store.dispatch(registerNewUserSucceeded(mockUser);
expect(store.getState()).toEqual({user: mockUser, status: 'idle'});
Edit
In response to the asker's edit, there is now a bug because of the confusing combination of await with .then.
Specifically, in onSubmit, you are doing both await and .then on the same promise. In this case, there is a race condition. The .then call happens first, and after that the await happens.
So instead of await user.register(data).then(...):
const onSubmit = async data => {
// ...
try {
await user.register(data);
} catch(e) {
console.log(e);
}
dispatch(createNewUser(data));
}
Here I'm only using await. the try/catch clause is instead of calling .catch on the promise.
using await lets you write as if you are writing synchronic code, so just write whatever you would put inside .then on the next line after an await expression.
Also in your register function:
async register(data) {
try {
let res = await axios.post(...);
return res;
} catch(e) {
console.log("error: ", e);
}
}
The state won't be updated instantly, as the server call is a promise. You should await something on the page the indicates the process is complete like this:
// Click on the Submit button
await act(async () => {
userEvent.click(screen.getByTestId('register-submit-button'));
await wait(() => getByText('Some text that appears after success '));
// verify the store was populated
console.log(await store.getState())
});
Or you can wait for the update:
// Click on the Submit button
await act(async () => {
userEvent.click(screen.getByTestId('register-submit-button'));
await act(() => sleep(500));
// verify the store was populated
console.log(await store.getState())
});
I'm trying to create a user profile that states that that profile is from one of the business owners in my app. It is supposed to create the profile and then merge info such as the 'roles' array with 'businessOwner' in it and also add the 'businessId'.
Sometimes, the code will work seamlessly. At other times, only the roles and the businessId will be passed to the created user (and all of the other information won't!).
async function writeToFirebase(values) {
authService.createUserWithEmailAndPassword(values.user.email, values.user.senha).then(
async function (user) {
userService.createUserProfileDocument(values.user)
const uid = user.user.uid
const userRef = await userService.doc(uid)
console.log('userRef', userRef)
try {
values.user.uid = uid
const { id } = await businessPendingApprovalService.collection().add(values)
await userRef.set({ roles: ['businessOwner'], businessId: id }, { merge: true })
} catch (error) {
console.error('error merging info')
}
},
function (error) {
var errorCode = error.code
var errorMessage = error.message
console.log(errorCode, errorMessage)
},
)
}
This is createUserWithEmailAndPassword:
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
try {
await userRef.set({
displayName,
email,
photoURL,
...additionalData,
providerData: providerData[0].providerId,
})
} catch (error) {
console.error('error creating user: ', error)
}
}
return this.getUserDocument(user.uid)
}
I think that the issue is on this line const snapshot = await userRef.get().
As stated in documentation you should fetch the snapshot using then() function in order to return the promise first.
I think you need to await on the below as well:-
await userService.createUserProfileDocument(values.user)
Since you are setting the user info here(await userRef.set), if you will not wait for the promise, then sometimes, your next block of code(await userRef.set({ roles: ['businessOwner'],) executes and after then your promise might get resolved. Because of this, you might not get the other information sometimes.
You also need to handle the error case of createUserProfileDocument.