22Jan
Create simple POS with React, Node and MongoDB #2: Auth state, Logout, Update Profile
Create simple POS with React, Node and MongoDB #2: Auth state, Logout, Update Profile

Defenition: POS – “Point of Sale”. At the point of sale, the merchant calculates the amount owed by the customer, indicates that amount, may prepare an invoice for the customer (which may be a cash register printout), and indicates the options for the customer to make payment.

This is the second chapter of our series of creating a simple POS with React, Node, and MongoDB. Today’s tutorial continues from where we left off in the previous chapter: adding register and login functionalities. Today we will add functionalities to handle authentication state and log out. We will also create a user profile where users can update user information.

Handle Auth State

Check if User is Logged In

Inside App.js, add this simple function to check the authentication state of the user.

const isLoggedIn = () => {
  return localStorage.getItem('TOKEN_KEY') != null;
};

We need to hide or show the header, sidebar, and footer on the register and login pages depending on whether the user is logged in or not. We use the above function to achieve this.

<Router>
         <Switch>
          <div>
            {isLoggedIn() && (
              <>
                <Header /> <Sidebar />
              </>
            )}
            <Route path="/register" component={Register} />
            <Route path="/login" component={Login} />
            <Route path="/dashboard" component={Dashboard} />
            {isLoggedIn() && <Footer />}
          </div>
        </Switch>
      </Router>

Create a Secured Route

We want to authorize only the logged in users to visit some of the pages in our app. Unauthenticated users must be redirected to the login page when attempted to visit such a page. Following SecuredRoute component will handle this functionality.

const SecuredRoute = ({ component: Component, ...rest }) => (
    
    <Route
      {...rest}
      render={props =>
      
        isLoggedIn() === true ? (
          <Component {...props} />
        ) : (
          <Redirect to="/login" />
        )
      }
    />
  );

Now place the dashboard component inside a secured route.

<SecuredRoute path="/dashboard" component={Dashboard} />

When we try to visit the dashboard now, we are redirected to the login page when not logged in.

Login page

Prevent Logged In User from Visiting the Login Page

Once users have successfully logged in, they must be prevented from visiting the login page again.

componentDidMount

componentDidMount() {
  if (localStorage.getItem("TOKEN_KEY") != null) {
      return this.props.history.goBack();
   }
 }

Every time a user tries to visit the login page, we check whether the token is present. If yes, we return the user to the last visited page.

Last visited page

Implement Logout

When a user logs out, we remove the token stored in the browser and redirect the user to the login page. We show the logout option inside the header menu to the logged in users.

To achieve this, first, remove the existing menu and replace it with the following code. It has additional HTML code to show and handle the logout option inside the dropdown menu.

      <div className="dropdown-menu dropdown-menu-lg dropdown-menu-right">
              <span className="dropdown-item dropdown-header">menu</span>
              <div className="dropdown-divider" />
              <a
                href="#"
                onClick={() => this.Logout()}
                className="dropdown-item"
              >
                <i className="fas fa-sign-out-alt mr-2" /> Logout
              </a>
            </div>

Now we can create the function that handles logging out. The function first prompts the user to confirm logout using a sweet alert. Then, it redirects the user back to the login page.

Import sweetalert and react-router-dom packages to the components/header/header.js file.

import swal from "sweetalert";
import { withRouter} from "react-router-dom";

We use withRouter to use the browser history API inside this file.

Inside the logout function, first, add a button to confirm or cancel the logout request. We use a switch to define button texts and values. If the confirm option is selected, the user is redirected to the login page after removing the token. Otherwise, nothing will be changed.

Logout = () => {
    swal("Are your sure SignOut?", {
      buttons: {
        nope: {
          text: "Let me back",
          value: "nope"
        },
        sure: {
          text: "I'm, Sure",
          value: "sure"
        }
      }
    }).then(value => {
      switch (value) {
        case "sure":
          swal(" SignOut Successfully", "success").then(val => {
            localStorage.removeItem("TOKEN_KEY");
            return this.props.history.push("/login");
          });
          break;
        case "nope":
          swal("Ok", "success");
          break;
        default:
          swal("Got away safely!");
      }
    });
  };

You can see how the logout functionality works in our app.

Logout functionality

Update User Profile

In this section, we will implement a profile page that allows updating of user data.

Create a new component named profile. Open the profile.js file.

Frontend

We can reuse the code from register.js and copy the required CSS code from AdminLTE example. Add first name, last name, phone number, and address fields to the profile. In the next chapter, we will provide more options for the user to update.

Add a hidden form to handle the user id to be able to identify the user at form submission.

showForm = ({
    values,
    errors,
    touched,
    handleChange,
    handleSubmit,
    onSubmit,
    isSubmitting,
    setFieldValue
  }) => {
    return (
      <form role="form" onSubmit={handleSubmit}>
        <div className="card-body">
         <input type="hidden" name="id" value={values._id} />
           <div className="form-group has-feedback">
            <label htmlFor="username">Username</label>
            <input
              onChange={handleChange}
              value={values.username}
              type="text"
              className={
                errors.username && touched.username
                  ? "form-control is-invalid"
                  : "form-control"
              }
              id="username"
              placeholder="Enter UserName"
            />
            <label htmlFor="username">First Name</label>
            <input
              onChange={handleChange}
              value={values.first_name}
              type="text"
              className={
                errors.first_name && touched.first_name
                  ? "form-control is-invalid"
                  : "form-control"
              }
              id="first_name"
              placeholder="Enter First Name"
            />
            {errors.first_name && touched.first_name ? (
              <small id="passwordHelp" class="text-danger">
                {errors.first_name}
              </small>
            ) : null}
          </div>
          <div className="form-group has-feedback">
            <label htmlFor="last_name">Last Name</label>
            <input
              onChange={handleChange}
              value={values.last_name}
              type="text"
              className={
                errors.last_name && touched.last_name
                  ? "form-control is-invalid"
                  : "form-control"
              }
              id="last_name"
              placeholder="Enter Last Name"
            />
            {errors.last_name && touched.last_name ? (
              <small id="passwordHelp" class="text-danger">
                {errors.last_name}
              </small>
            ) : null}
          </div>
          <div className="form-group has-feedback">
            <label htmlFor="phone">phone number</label>
            <input
              onChange={handleChange}
              value={values.phone}
              type="text"
              className={
                errors.phone && touched.phone
                  ? "form-control is-invalid"
                  : "form-control"
              }
              id="phone"
              placeholder="Enter phone number"
            />
            {errors.phone && touched.phone ? (
              <small id="passwordHelp" class="text-danger">
                {errors.phone}
              </small>
            ) : null}
          </div>
          <div className="form-group has-feedback">
            <label htmlFor="address">address</label>
            <textarea
              onChange={handleChange}
              value={values.address}
              className={
                errors.address && touched.address
                  ? "form-control is-invalid"
                  : "form-control"
              }
              id="address"
              placeholder="Address"
            />
            {errors.address && touched.address ? (
              <small id="passwordHelp" class="text-danger">
                {errors.address}
              </small>
            ) : null}
          </div>
        </div>
        {}
        <div className="card-footer">
          <button
            type="submit"
            disabled={isSubmitting}
            className="btn btn-block btn-primary"
          >
            Save
          </button>
        </div>
      </form>
    );
  };

Update the Yup validation function as the following code shows.

const ProfileSchema = Yup.object().shape({
  username: Yup.string()
    .min(2, "username is Too Short!")
    .max(50, "username is Too Long!")
    .required("username is Required"),
  first_name: Yup.string()
    .min(2, "firstname is Too Short!")
    .max(30, "firstname is Too Long!")
    .required("firstname is Required"),
  last_name: Yup.string()
    .min(2, "lastname is Too Short!")
    .max(30, "lastname is Too Long!")
    .required("lastname is Required"),
  phone: Yup.number("Phone number is use only number")
    .min(10, "Phone number must be 10 characters!")
    .required("Phone number is Required"),
  address: Yup.string()
    .min(12, "address is Too Short!")
    .max(50, "address is Too Long!")
    .required("address is Required"),
  email: Yup.string()
    .email("Invalid email")
    .required("Email is Required")
});

Populate the Form

After loading the form, fill its fields with the stored user data. Here is how we can do this.

Get the User Id from the JWT Token

The only way to identify the logged-in user is the JWT token stored in the local storage. If you can remember, we stored the user id inside the JWT token during token creation. However, this data is encrypted. So we need to decode the data to retrieve the associated user id. Following parseJwt function decodes the token and returns the user id.

parseJwt() {
    let token = localStorage.getItem("TOKEN_KEY");
    var base64Url = token.split(".")[1];
    var base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
    var jsonPayload = decodeURIComponent(
      atob(base64)
        .split("")
        .map(function(c) {
          return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2);
        })
        .join("")
    );

    return JSON.parse(jsonPayload);
  }

Now, create a function that store the user id returned from the above function in the component state.

getData = async id => {
    await axios
      .get("http://localhost:8080/profile/id/" + id)
      .then(response => {
        this.setState({ response: response.data });
      })
      .catch(error => {
        this.setState({ error_message: error.message });
      });
  };

On componentDidMount event, retrieve the user id and get user data.

componentDidMount() {
    let { id } = this.parseJwt();
    this.getData(id);
  }

Attach the state to Formik initialValues variable to populate the form using the data retrieved after loading the page.

                 <Formik
                    enableReinitialize={true}
                    initialValues={
                      result
                        ? result
                        : {
                            id: "",
                            username: "",
                            email: "",
                            first_name: "",
                            last_name: "",
                            phone: "",
                            address: ""
                          }
                    }

Backend

In the backend, add the route that sends user data.

app.get("/profile/id/:id", async (req, res) => { 
   let doc = await Users.findOne({ _id: req.params.id });
   res.json(doc);
});

You can see how the profile page now looks like.

Re populate profile page

Handle Form Submission and Avatar

Now we can get back to the task at our hand: uploading an avatar, storing user data, and displaying the user profile.

In the frontend, we add a new form field to the Formik object to add a file using the setFieldValue option.\

let result = this.state.response;<span style={{ color: "#00B0CD", marginLeft: 10 }}>
Add Picture</span>
          <div className="form-group">
            <label htmlFor="exampleInputFile">Avatar upload</label>
            <div className="input-group">
              <div className="custom-file">
                <input
                  type="file"
                  onChange={e => {
                    e.preventDefault();
                    setFieldValue("avatars", e.target.files[0]); 
                     // for upload
                    setFieldValue(
                      "file_obj",
                      URL.createObjectURL(e.target.files[0])
                    ); // for preview image
                  }}
                  name="avatars"
                  className={
                    errors.email && touched.email
                      ? "form-control is-invalid"
                      : "form-control"
                  }
                  accept="image/*"
                  id="avatars"
                  className="custom-file-input"
                  id="exampleInputFile"
                />
                <label className="custom-file-label" htmlFor="exampleInputFile">
                  Choose file
                </label>
              </div>
              <div className="input-group-append">
                <span className="input-group-text" id>
                  Upload
                </span>
              </div>
            </div>
          </div>

When the form is submitted, we will create a new form object and append the form data including the uploaded file.

onSubmit={(values, { setSubmitting }) => {
            let formData = new FormData();
            formData.append("id", values._id);
            formData.append("username", values.username);
            formData.append("first_name", values.first_name);
            formData.append("last_name", values.last_name);
            formData.append("phone", values.phone);
            formData.append("address", values.address);
            formData.append("email", values.email);
              if (values.avatars) {
                  formData.append("avatars", values.avatars);
              }
             
            this.submitForm(formData, this.props.history);
                  setSubmitting(false);
            }}
            validationSchema={ProfileSchema}

We have to update the AJAX request that submits the form to use a PUT request instead of POST.

submitForm = async formData => {
    await axios
      .put("http://localhost:8080/profile", formData)
      .then(res => {
        if (res.data.result === "success") {
          swal("Success!", res.data.message, "success")
        } else if (res.data.result === "error") {
          swal("Error!", res.data.message, "error");
        }
      })
      .catch(error => {
        console.log(error);
        swal("Error!", "Unexpected error", "error");
      });
  };

Backend

Now we have to update the user schema to add the new data fields.

const schema = mongoose.Schema({
  avatars: String,
  username: String,
  email: String,
  first_name: { type: String, default: "" },
  last_name: { type: String, default: "" },
  phone: { type: String, default: "" },
  address: { type: String, default: "" },
  password: String,
  level: { type: String, default: "staff" },
  created: { type: Date, default: Date.now }
});

We have to add a few new packages to handle the submitted data. Formidable handles the form object. Path and fs packages handle file management.

const formidable = require("formidable");
const path = require("path");
const fs = require("fs-extra");
app.use(express.static(__dirname + "/uploaded"));

Also, we have to create a new public directory to store the images.

We add a function to handle the form submission. Formidable can be used to parse form data.

app.put("/profile", async (req, res) => {
  try {
    var form = new formidable.IncomingForm();
    form.parse(req, async (err, fields, files) => {
      let doc = await Users.findOneAndUpdate({ _id: fields.id }, fields);
      await uploadImage(files, fields);
    res.json({ result: "success", message: "Update Successfully" });
    });
  } catch (err) {
    res.json({ result: "error", message: err.errmsg });
  }
});

We need another function to handle file upload.

First, we will rename the avatar image and move it to a directory of our choice. We add the logic to create such a directory if it does not exist. Lastly, we update the user’s data stored in the database.

uploadImage = async (files, doc) => {
  if (files.avatars != null) {
    var fileExtention = files.avatars.name.split(".").pop();
    doc.avatars = `${Date.now()}+${doc.username}.${fileExtention}`;
    var newpath =
      path.resolve(__dirname + "/uploaded/images/") + "/" + doc.avatars;

    if (fs.exists(newpath)) {
      await fs.remove(newpath);
    }
    await fs.move(files.avatars.path, newpath);

    await Users.findOneAndUpdate({ _id: doc.id }, doc);
  }
};
Resulting profile page

The resulting profile page can be seen here.

When a user picks an image for the form, we need to show its preview to the user. When the user reloads the profile page, the uploaded image or a default image must appear on the top.

We will create a new function named showPreviewImage. It listens to the file picker event of the file_obj form field and shows a default image if no image is picked.

showPreviewImage = values => {
    return (
      <div class="text-center">
        <img
          id="avatars"
          src={
            values.file_obj != null
              ? values.file_obj
              : "http://localhost:8080/images/user.png"
          }
          class="profile-user-img img-fluid img-circle"
          width={100}
        />
      </div>
    );
  };

This is our completed profile page.

Completed profile page
Completed profile page

When the user visits the profile page after updating the user data, it must show the avatar on the top of the page.

We can easily implement this by updating the src attribute with the avatar attribute of the user object.

getData = async id => {
    await axios
      .get("http://localhost:8080/profile/id/" + id)
      .then(response => {
        document.getElementById("avatars").src = "http://localhost:8080/images/"+response.data.avatars
        this.setState({ response: response.data });
      })
      .catch(error => {
        this.setState({ error_message: error.message });
      });
  };
Prevent redirect back to login

Now we will see the following page when we visit the user profile.

Conclusion

In this chapter, we learned how to check the authentication state of a user. We also handled the user logout functionality. We created a new user profile page where a user can update user data and upload an avatar. In the next chapter, we will implement how to send an account activation email and handle account activation. You will learn how to establish an email pipeline where we can communicate with customers to inform about new promotions or send daily reports. Here you can find the GitHub repo for this chapter.

Credit

Protected routes and authentication with React Router v4
Icon made by Pixel perfect from www.flaticon.com

Previous lessons

Create a Simple POS with React, Node and MongoDB #0: Initial Setup Frontend and Backend
Create a simple POS with React, Node and MongoDB #1: Register and Login with JWT

Developer Relation @instamobile.io

3 Replies to “Create simple POS with React, Node and MongoDB #2: Auth state, Logout, Update Profile”

  1. alinaitova91 5 years ago

    Hey, thanks, is this authentification handling works for POS only, or pretty much for any kind of app?

    1. Krissanawat Kaewsanmuang 5 years ago

      Yup you can use for any kind app but now I build for POS app

  2. Every time an authentication token is saved in the local storage, a kitten is killed. You can read a lot about why this is insecure, especially if the token is long-lived (30 days in your example). Sometimes it’s the only way but it is rarely the case. Secure and httpOnly cookies are preffered but they are really tricky. Also refresh token is a must if you are going to save it for a long time.

    I still appreciate what you are trying to teach, keep it up! Authentication is very hard to get right and not beginner friendly 😫.

Leave a Reply