内容简介: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 hand
Create simple POS with React, Node and MongoDB #2: Auth state, Logout, Update Profile
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"> {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"> {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"> {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"> {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> <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
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
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。