We are building a decoupled E-commerce site with Gatsby and Drupal commerce. As you are well aware of the fact that in all the web applications one of the most important features is user authenticated browsing. I will not go into the details of why user-authenticated browsing is important as you will find plenty of blog posts on that.
This blog post is aimed at users who may find themselves struggling like I did while trying to add the user authentication functionality to a Gatsby site. So let us get started.
Goal
- User should be able to register
- User should be able to log in as an authenticated user
- User should be able to add products to their cart as an authenticated user
Prerequisite
- Your Drupal commerce site should be up and going with all the commerce modules enabled that are provided by default.
- You should be able to fetch your Drupal data in your Gatsby site.
- Also, we will need the commerce cart API module which provides a RESTful interface to interact with our cart in Drupal.
Let’s Get started
- Go to REST option under web services and enable all the cart and user resources with the below permissions.
We are done from the Drupal end here. Let’s move to the Gatsby end now.
On Gatsby End
1. Register
The first thing we will do is add user registration functionality.
export const registerUser = async (name, password, email) => {
const token = await fetch(`${url}rest/session/token?value`);
const sessionToken = await token.text();
if (sessionToken) {
const res = await fetch(`${url}user/register?_format=hal_json`, {
method: 'POST',
headers: {
'Content-Type': 'application/hal+json',
'X-CSRF-TOKEN': sessionToken,
},
body: JSON.stringify({
_links: {
type: {
href: `${url}rest/type/user/user`,
},
},
name: { value: name },
mail: { value: email },
pass: { value: password },
}),
});
const data = await res.json();
return data;
}
};
Create your UserRegistration form and pass all the valid arguments to the registerUser function. Now submit your form to see your user registered on the Drupal end under the People tab. In case you get any permission issues, check under config/people/accounts to see if visitors are allowed to register.
Now that our user is registered. Our next step is to log in.
2. Login
Our log in functionality is based on the React Context API. So it is necessary you know how the Context API works.
Visit this link and copy four of the below-mentioned files:
- drupalOauth.js.
- drupalOauthContext.js
- withDrupalOauthConsumer.js
- withDrupalOauthProvider.js
Place all four files in a single directory named drupal-OAuth. Next, wrap your base component with DrupalOAuthConsumer to initialise the context provider. Your base component will look something like this:
import drupalOauth from '../components/drupal-oauth/drupalOauth';
import withDrupalOauthProvider from '../components/drupal-oauth/withDrupalOauthProvider';
// Initialize a new drupalOauth client which we can use to seed the context provider.
const drupalOauthClient = new drupalOauth({
drupal_root: 'your drupal root url',
client_id: 'your simple OAuth consumer Id',
client_secret: 'Your simple OAuth consumer key',
});
// ... the component definition goes here ...
export default withDrupalOauthProvider(drupalOauthClient, Layout)
Now to create your sign in or login form take a look at below code:
import React, {Component} from 'react';
import { FaSpinner } from 'react-icons/fa';
import withDrupalOauthConsumer from '../DrupalOauth/withDrupalOauthConsumer';
class SignIn extends Component {
constructor(props){
super(props);
this.handleSubmit=this.handleSubmit.bind(this);
}
state = {
processing: false,
username: '',
password: '',
error: null,
};
handleSubmit = () => {
event.preventDefault();
this.setState({ processing: true });
const { username, password } = this.state;
if(!username && !password) {
this.setState({ processing: false });
this.setState({error: "User name and password doesn't exist"})
} else {
this.props.drupalOauthClient.handleLogin(username, password, '').then((res) => {
localStorage.setItem('username', JSON.stringify(username));
if(res !==undefined){
this.setState({ open: false, processing: false });
this.setState({ error: 'You are now logged in'});
this.props.updateAuthenticatedUserState(true);
setTimeout(() => {
document.location.href="/";
}, 3000);
} else {
this.setState({ processing: false });
this.setState({error: "User name and password doesn't exist"})
}
});
}
};
render() {
const { error, processing } = this.state;
return (
Login Now!
{error && {error}
}
);
}
}
export default withDrupalOauthConsumer(SignIn);
When you submit the form Drupal will take care of generating the OAuth token and return it to you. To check this you can wrap your component with DrupalOAuthConsumer, and check via the props.userAuthenticated.
To understand in-depth how the code works. You can follow this link.
One thing to note here is that the above code does not take into account the user login on Drupal end. So to be able to log in on Drupal end add the drupalLogIn code to your drupalOauth.js file and call it inside the fetchOauthToken function. So that every time user tries to log in on Gatsby end, user session get’s initiated on Drupal end as well.
/**
* Login request to Drupal.
*
* Exchange username and password.
* @param username
* @param password
* @returns {Promise}
* Returns a promise that resolves to JSON response from Drupal.
*/
const drupalLogIn = async (username, password) => {
const response = await fetch(loginUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: username,
pass: password,
}),
});
if (response.ok) {
const json = await response.json();
if (json.error) {
throw new Error(json.error.message);
}
return json;
}
Remember we are only taking into account the login functionality here. If you are trying to implement the logout functionality as well, make the below piece of code work same as login.
/**
* Logout request to Drupal.
*
* Logs the user out on drupal end.
*/
const drupalLogout = async () => {
const oauthToken = await isLoggedIn();
const logoutoken = oauthToken.access_token;
if (logoutoken) {
const res = await fetch(`${process.env.GATSBY_DRUPAL_ROOT}/user/logout?_format=json`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${logoutoken}`,
},
});
if (res.ok) {
return true;
}
}
};
Also, take into account that drupalOauth.js is a class service. So drupalLogin and drupalLogout are the implementation of a class and need some modifications.
Authenticated Commerce Cart
Now that our user is logged in and registered, our next step is to post the data to our commerce cart.
If you go through the commerce cart API documentation. It explains how commerce cart API module works. To post data to the cart as an authenticated user you must be logged in. Once you are logged in. We can POST, GET, UPDATE our cart. Go through below code. Which is fairly simple to understand. We are just taking the access token generated by simple OAuth from Drupal end on login that we have already stored in our browser local storage and sending it as a bearer token as part our request header to the Drupal end so it can recognise that the user is Authenticated.
import axios from 'axios';
const TokenGenerator = require('uuid-token-generator');
const url = process.env.GATSBY_CART_API_URL;
class CartService {
getCartToken() {
const tokgen = new TokenGenerator();
const oauthToken = JSON.parse(localStorage.getItem('drupal-oauth-token'));
var myHeaders = new Headers();
let cartToken = '';
if(!oauthToken) {
cartToken = (localStorage.getItem('cartToken') !== null) ? JSON.parse(localStorage.getItem('cartToken')) : tokgen.generate();
myHeaders.append('Commerce-Cart-Token', cartToken);
myHeaders.append('Content-Type', 'application/json');
localStorage.setItem('cartToken', JSON.stringify(cartToken));
} else {
cartToken = oauthToken.access_token;
localStorage.setItem('cartToken', JSON.stringify(cartToken));
myHeaders.append('Authorization' , `Bearer ${cartToken}`,);
myHeaders.append('Content-Type', 'application/json',);
}
return myHeaders;
}
getCartItem = async () => {
const header = await this.getCartToken();
const res = await fetch(`${url}cart?_format=json`, {
method: 'GET',
headers: header
});
const cartData = await res.json();
return cartData;
}
addCartItem = async (id, quantity) => {
const header = this.getCartToken();
const res = await fetch(`${url}cart/add?_format=json`, {
method: 'POST',
headers: header,
body: JSON.stringify([{
purchased_entity_type: 'commerce_product_variation',
purchased_entity_id: id,
quantity: quantity
}])
})
const data = await res.json();
return data;
}
updateCartItem = async (quantity, order_item_id, order_id) => {
const header = this.getCartToken();
const res = await fetch(`${url}cart/${order_id}/items/${order_item_id}?_format=json`, {
method: 'PATCH',
headers: header,
body: JSON.stringify({
"quantity": quantity
})
})
const data = await res.json();
return data;
}
removeCartItem = async(order_id, order_item_id) => {
const header = this.getCartToken();
const res = await fetch(`${url}cart/${order_id}/items/${order_item_id}?_format=json`,{
method: 'Delete',
headers: header,
})
if (res.status == 204) {
const data = await this.getCartItem()
return data;
}
}
removeCart = async(order_id) => {
const header = this.getCartToken();
const res = await fetch(`${url}cart/${order_id}/items?_format=json`,{
method: 'Delete',
headers: header,
})
if (res.status == 204) {
const data = await this.getCartItem()
return data;
}
}
}
const CartHandler = new CartService();
export default CartHandler;
This will allow you to post the cart data as an anonymous user when you are logged in as well as authenticated user once you are logged in. (Add uuid-token-generator) to your packages to make it work.
To add a product to your cart you can simply import the CartService class into your component and use it as :
import CartHandler from '../Services/CartService';
CartHandler.addCartItem(variationId, quantity);
This is it. Cheers! We are done here. We have been able to successfully register the user, authenticate the user and post data to our commerce cart.
P.S - If you face any issues. Kindly mention in the comments.