--- tags: authentication, react, spring --- # Authenticating the User - Part 1 (2233) We will look at authenticating a user and controlling access to pages based on your logged-in status. ## Backend Updates First let's make some changes to the Spring Boot backend application to accomdate what we are adding. ### application.yml Add the following in the indicated sections: 1. To **allowed-hearers** add `Expires` and `pragma` 2. To **allowed-public-apis** add ``` - /api/login - /api/register - /api/logout - /api/category/ - /api/comment/{postId:\w+} - /api/post/** - /api/post/{postId:\w+} - /api/post/c/{slug:\w+} ``` Note that the first three replace `/login`, `/register`, and `/logout` ### FirebaseAuthenticationConfig Replace: ```java! .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.ALWAYS); ``` with: ```java! .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); ``` This change makes our server session stateless. This is common with REST APIs. In computer networking, a stateless session refers to a communication session between two endpoints (typically client and server) in which the server does not maintain any information about previous interactions with the client. This means that each request from the client is treated as an independent, self-contained transaction, and the server does not keep track of any context or state related to previous requests. Stateless sessions are often used in systems that need to handle a large number of requests from many clients simultaneously, such as web servers or load balancers. By not maintaining any state information between requests, the server can handle requests more efficiently and scale better. However, stateless sessions have some limitations. For example, they cannot be used for applications that require continuous communication between the client and server, such as online gaming or real-time chat applications. In these cases, stateful sessions are typically used, in which the server maintains information about the client's state and context throughout the session. ## Front End Additions Now we will update and add a few pages to the front-end. ### AuthContext First is the AuthContext. Add this new new file to the `src` folder. In ReactJS, "context" refers to a feature that allows data to be passed down through the component tree without having to pass props down manually at every level. Context provides a way for components to share data without having to explicitly pass it through each component in the tree. The Context API allows you to create a shared state that can be accessed by any component in the component tree. Context is useful for sharing data that is global to the application, such as the currently authenticated user, the theme of the application, or the language preference. In order to use context in React, you need to create a context object using the createContext() method. This creates a new context object that you can use to store data. You then provide this context object to any components that need to access it by wrapping those components in a <Context.Provider> component. Any components that need to access the data stored in the context can then use the useContext() hook to access it. Here is an example of how you might use context in React: ```javascript= // Define the context const MyContext = React.createContext(defaultValue); // Use the context in a component function MyComponent() { const contextData = useContext(MyContext); // ... } ``` In this example, `MyContext` is a context object that has been created using the `createContext()` method. `defaultValue` is an optional parameter that specifies the default value of the context. `MyComponent` is a component that uses the context data by calling the `useContext()` hook to retrieve the data stored in the context. ```javascript= import React, {createContext, useState} from "react"; const AuthContext = createContext({ currentUser: {}, setCurrentUser: ()=>{}, isLoggedIn: false, login: () => {} }); const AuthProvider = ({children}) => { const [isLoggedIn, setIsLoggedIn] = useState(false); const [currentUser, setUser] = useState(null); //REPLACE WITH DATA FROM YOUR FIREBASE const fakeUser = { "userId": "Zp82RQjbR94JXX7GLdAO", "uid": "dM6DHwxsx2VmNXIo2B2BiTWlL8C2", "username": "vac", "email": "vanessa.coote@gmail.com", "firstName": "Vanessa", "middleName": "", "lastName": "Coote", "intro": "\"While we teach, we learn\" - Seneca", "profile": "FAMU Instructor", "mobile": "8504275373", "registeredAt": { "seconds": 1674896220, "nanos": 973000000 }, "lastLogin": null }; const setCurrentUser = (user) => setUser(user) const login = (email, password) => { //TODO: Add firebase log in with backend token setCurrentUser(fakeUser); setIsLoggedIn(true); } return ( <AuthContext.Provider value={{ currentUser, isLoggedIn, setCurrentUser, login }} > {children} </AuthContext.Provider> ); } const AuthConsumer = AuthContext.Consumer; export{AuthContext, AuthConsumer, AuthProvider}; ``` This implementation fakes the login process and automatically marks a user as signed in. Later we will replace this with Firebase authentication code that interacts with our Spring Boot login process. Next, we need to the apply the context to the application. To do this, we will need to apply the context in the `App.js` file. To do this, wrrap everything in your return statement in a `<AuthProvider>` tag. ```jsx= function App() { return ( <AuthProvider> <Router> <div className="container"> <Menu /> <Routes> <Route element={<Home/>} path="/" index/> <Route element={<IndividualPost/>} path="/post/:postId" /> <Route element={<Category/>} path="/:slug" /> </Routes> </div> <Footer /> </Router> </AuthProvider> ); } ``` ### Private Routing Now that we have a way to authenticate a user, we need to set up pages that will be private. Private pages are pages that require you to log in to view them. All the pages we have created so far are publicly available pages. To make private routing possible, we will add a new stateless component called `PrivateRoute` to the `src` folder. #### PrivateRoute ```jsx= import React, {useContext} from 'react'; import {AuthContext} from "./AuthContext"; import {Navigate} from "react-router-dom"; function PrivateRoute({children}) { const {currentUser, isLoggedIn} = useContext(AuthContext); if(isLoggedIn && currentUser?.length > 0) { localStorage.setItem("user", JSON.stringify(currentUser)); return children; } return <Navigate to="/signin" replace={true} /> } export default PrivateRoute; ``` This function will receive the children components (tags that are inside of the PrivateRoute tag) and render them only is the isLoggedIn variable is true and the currentUser variable is not empty. If either fails, the user will be routed to the signin page. #### MyPosts This is a new component that should be added to the `pages` folder. This page will show the user all their posts. It will allow the user to delete or unpublish a post. Finally it will include an edit button that takes them to a page to edit the post details. ```jsx= import React, { Component } from 'react'; import axios from 'axios'; import { Link } from 'react-router-dom'; import {AuthContext} from "../AuthContext"; class MyPosts extends Component { static contextType = AuthContext; constructor(props) { super(props); this.state = { posts: null }; this.userId = this.context.currentUser.userId; } componentDidMount() { this.fetchPosts().then(null); } fetchPosts = async () => { await axios .get(`http://localhost:8080/api/post/user/${this.userId}`) .then(response => { this.setState({ posts: response.data.posts }); }) .catch(error => { console.error('Error fetching posts:', error); }); }; handlePublishChange = async (postId, newValue) => { await axios .put(`http://localhost:8080/api/post/${postId}`, { published: newValue }) .then(response => { // update state with new post const { posts } = this.state; const index = posts.findIndex(post => post.postId === postId); posts[index].published = newValue; this.setState({ posts }); }) .catch(error => { console.error('Error updating post:', error); }); }; handleDeletePost = async postId => { await axios .delete(`http://localhost:8080/api/post/${postId}`) .then(response => { // remove deleted post from state const { posts } = this.state; const updatedPosts = posts.filter(post => post.postId !== postId); this.setState({ posts: updatedPosts }); }) .catch(error => { console.error('Error deleting post:', error); }); }; render() { return ( <> <div className="row"> <div className="col-md-10 col-sm-12"> <h4 className="display-4 mt-3">My Posts</h4> </div> <div className="col-2 text-md-end text-sm-start"> <Link to="/post/new" className="btn btn-outline-primary mt-md-5 mt-sm-1"> <i className="fa-regular fa-plus"></i> Add Post </Link> </div> </div> <div className="card mt-2"> <div className="card-body"> <div className="table-responsive"> <table className="table table-striped"> <thead> <tr> <th>Published</th> <th>Title</th> <th>Summary</th> <th>Created At</th> <th></th> </tr> </thead> <tbody> { this.state.posts && this.state.posts.map((post) => ( <tr key={post.postId}> <td> <div className="form-check form-switch"> <input className="form-check-input " role="switch" type="checkbox" onChange={() => this.handlePublishChange(post.postId, !post.published)} {...post.published && {defaultChecked: true}} /> </div> </td> <td>{post.title}</td> <td>{post.summary}</td> <td>{new Date(post.createdAt.seconds * 1000).toLocaleString()}</td> <td> <div className="btn-group" role="group"> <button type="button" className="btn btn-sm btn-danger" data-bs-toggle="modal" data-bs-target={"#deleteModal" + post.postId} > <i className="fa fa-trash"></i> </button> <Link to={"/post/" + post.postId + "/edit"} className="btn btn-sm btn-primary" > <i className="fa fa-pencil"></i> </Link> </div> <div className="modal fade" id={"deleteModal" + post.postId} tabIndex="-1" role="dialog" aria-labelledby={"deleteModalLabel" + post.postId} aria-hidden="true" > <div className="modal-dialog modal-dialog-centered" role="document"> <div className="modal-content"> <div className="modal-header"> <h5 className="modal-title" id={"deleteModalLabel" + post.postId} > Delete Post </h5> <button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close" > </button> </div> <div className="modal-body"> Are you sure you want to delete this post? </div> <div className="modal-footer"> <button type="button" className="btn btn-secondary" data-bs-dismiss="modal" > Cancel </button> <button type="button" className="btn btn-danger" onClick={() => this.handleDeletePost(post.postId)} data-bs-dismiss="modal" > Delete </button> </div> </div> </div> </div> </td> </tr> )) } </tbody> </table> </div> </div> </div> </> ); } } export default MyPosts; ``` Now let's add this route to the `App.js` file. Because this route is private (requires login), it will look a little different. ```jsx= <Route element={<PrivateRoute><MyPosts/></PrivateRoute>} path="/posts"/> ``` All private routes, will need to wrapped in the `<PrivateRoute>` tag. ### Toasts In web development, a "toast" is a small, unobtrusive pop-up message that appears on a webpage to provide a notification or alert to the user. Toasts typically appear in the corner or bottom of the screen, and are designed to be non-intrusive, so they don't interrupt the user's workflow. Toasts are often used to provide feedback to the user after an action has been completed, such as confirming that a form has been submitted successfully, or notifying the user that an error has occurred. They can also be used to provide contextual information or reminders to the user, such as a reminder that a subscription is about to expire. Toasts are typically designed to be simple and easy to dismiss. They may include a short message, an icon, and a button or link to take an action or dismiss the message. Some toasts may also include a time-out feature, so they automatically disappear after a set amount of time. Bootstrap includes a library to handle this. We will make a stateless component calles `Toast` under `fragments`. This component will accept a color and message. The color will be the Bootstrap color classes (ie, success or danger). ```jsx= import React from 'react'; function Toast({color, message}) { return ( <div id="liveToast" className={`toast align-items-center text-bg-${color} border-0`} role="alert" aria-live="assertive" aria-atomic="true"> <div className="d-flex"> <div className="toast-body"> {message} </div> <button type="button" className="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button> </div> </div> ); } export default Toast; ``` ### Edit Post ```jsx= const EditPost = () => { const {postId} = useParams(); const [title, setTitle] = useState(""); const [content, setContent] = useState(""); const [summary, setSummary] = useState(""); const [metaTitle, setMetaTitle] = useState(""); const [slug, setSlug] = useState(""); const [categoryOptions, setCategoryOptions] = useState([]); const [category, setCategory] = useState([]); const [tags, setTags] = useState([]); const [toastMessage, setToastMessage] = useState("Post successfully updated."); const [toastColor, setToastColor] = useState("success"); useEffect( () => { getCategories().then( async () => getPost().then(null)); }, [postId]); const showToast = () => { const toastLiveExample = document.getElementById('liveToast') const toastBootstrap = bootstrap.Toast.getOrCreateInstance(toastLiveExample) toastBootstrap.show() } const getPost = async ()=>{ await axios .get("http://localhost:8080/api/post/" + postId) .then((response) => { const post = response.data.post; setTitle(post.title); setSummary(post.summary); setContent(post.content); setSlug(post.slug); setMetaTitle(post.metaTitle); setCategory(post.categories) setTags(post.tags) }) .catch((error) => console.error(error)); } const getCategories = async () => { await axios .get("http://localhost:8080/api/category/") .then((response) => setCategoryOptions(response.data.categories)) .catch((error) => console.error(error)); } const handleSubmit = async (event) => { event.preventDefault(); if (!title || !content || !metaTitle || category.length === 0 || tags.length === 0) { setToastColor("danger"); setToastMessage("Please fill in all required fields."); showToast(); return; } if(summary === "") setSummary( content.substring(0, content.length > 300 ? 300 : content.length) + "..."); let data = { title, content, metaTitle, tags, summary, categoryId: category, createdAt: Date.now().toLocaleString() } await axios.put("http://localhost:8080/api/post/" + postId, data).then(()=>{ }).catch(e => { console.log(e); setToastColor("danger"); setToastMessage("An error occurred and the post was not updated.") }).finally(() =>{ showToast(); }) }; return ( <> <div className=" mt-3"> <h1>Create New Post</h1> <form onSubmit={handleSubmit}> <div className="form-group"> <label htmlFor="title">Title*</label> <input type="text" className="form-control" id="title" placeholder="Enter post title" value={title} onChange={(event) => setTitle(event.target.value)} required /> </div> <div className="form-group"> <label htmlFor="content">Content*</label> <textarea className="form-control" id="content" rows="5" placeholder="Enter post content" value={content} onChange={(event) => setContent(event.target.value)} required ></textarea> </div> <div className="form-group"> <label htmlFor="summary">Summary</label> <textarea className="form-control" id="summary" rows="3" placeholder="Enter post summary" value={summary} onChange={(event) => setSummary(event.target.value)} ></textarea> </div> <div className="form-group"> <label htmlFor="metaTitle">Meta Title*</label> <input type="text" className="form-control" id="metaTitle" placeholder="Enter post meta title" value={metaTitle} onChange={(event) => setMetaTitle(event.target.value)} required /> </div> <div className="form-group"> <label htmlFor="slug">Slug</label> <input type="text" className="form-control" id="slug" placeholder="Enter post slug" value={slug} onChange={(event) => setSlug(event.target.value)} /> </div> <div className="form-group"> <label htmlFor="category">Category*</label> <select className="form-control" id="category" multiple value={category} onChange={(event) => setCategory(Array.from(event.target.selectedOptions, (option) => option.value))} required > {categoryOptions.map((category) => ( <option key={category.categoryId} value ={category.categoryId}> {category.title} </option> ))} </select> </div> <div className="form-group"> <label htmlFor="tags">Tags*</label> <input type="text" className="form-control" id="tags" placeholder="Enter post tags (comma-separated)" value={tags} onChange={(event) => setTags(event.target.value.split(",").map((tag) => tag.trim()))} required /> </div> <button type="submit" className=" mt-3 btn btn-primary float-right"> Save </button> </form> </div> <Toast message={toastMessage} color={toastColor} /> </> ); }; export default EditPost; ``` ### New Post ```jsx= const NewPost = () => { const [title, setTitle] = useState(""); const [content, setContent] = useState(""); const [summary, setSummary] = useState(""); const [metaTitle, setMetaTitle] = useState(""); const [slug, setSlug] = useState(""); const [categoryOptions, setCategoryOptions] = useState([]); const [category, setCategory] = useState([]); const [tags, setTags] = useState([]); const [toastMessage, setToastMessage] = useState("Post successfully created."); const [toastColor, setToastColor] = useState("success"); useEffect(async () => { await axios .get("http://localhost:8080/api/category/" ) .then((response) => setCategoryOptions(response.data.categories)) .catch((error) => console.error(error)); }, []); const showToast = () => { const toastLiveExample = document.getElementById('liveToast') const toastBootstrap = bootstrap.Toast.getOrCreateInstance(toastLiveExample) toastBootstrap.show() } const handleSubmit = async (event) => { event.preventDefault(); if (!title || !content || !metaTitle || category.length === 0 || tags.length === 0) { setToastColor("danger"); setToastMessage("Please fill in all required fields."); showToast(); return; } if(summary === "") setSummary( content.substring(0, content.length > 300 ? 300 : content.length) + "..."); let data = { title, content, metaTitle, tags, summary, categoryId: category, createdAt: Date.now().toLocaleString() } await axios.post("http://localhost:8080/api/post/", data) .then((response) => { // reset form fields setTitle(""); setContent(""); setSummary(""); setMetaTitle(""); setSlug(""); setCategory([]); setTags([]); setToastColor("success"); setToastMessage("Post successfully created.") }).catch(e => { console.log(e); setToastColor("danger"); setToastMessage("An error occurred and the post was not created.") }).finally(() =>{ showToast(); }) }; return ( <> <div className=" mt-3"> <h1>Create New Post</h1> <form onSubmit={handleSubmit}> <div className="form-group"> <label htmlFor="title">Title*</label> <input type="text" className="form-control" id="title" placeholder="Enter post title" value={title} onChange={(event) => setTitle(event.target.value)} required /> </div> <div className="form-group"> <label htmlFor="content">Content*</label> <textarea className="form-control" id="content" rows="5" placeholder="Enter post content" value={content} onChange={(event) => setContent(event.target.value)} required ></textarea> </div> <div className="form-group"> <label htmlFor="summary">Summary</label> <textarea className="form-control" id="summary" rows="3" placeholder="Enter post summary" value={summary} onChange={(event) => setSummary(event.target.value)} ></textarea> </div> <div className="form-group"> <label htmlFor="metaTitle">Meta Title*</label> <input type="text" className="form-control" id="metaTitle" placeholder="Enter post meta title" value={metaTitle} onChange={(event) => setMetaTitle(event.target.value)} required /> </div> <div className="form-group"> <label htmlFor="slug">Slug</label> <input type="text" className="form-control" id="slug" placeholder="Enter post slug" value={slug} onChange={(event) => setSlug(event.target.value)} /> </div> <div className="form-group"> <label htmlFor="category">Category*</label> <select className="form-control" id="category" multiple value={category} onChange={(event) => setCategory(Array.from(event.target.selectedOptions, (option) => option.value))} required > {categoryOptions.map((category) => ( <option key={category.categoryId} value ={category.categoryId}> {category.title} </option> ))} </select> </div> <div className="form-group"> <label htmlFor="tags">Tags*</label> <input type="text" className="form-control" id="tags" placeholder="Enter post tags (comma-separated)" value={tags} onChange={(event) => setTags(event.target.value.split(",").map((tag) => tag.trim()))} required /> </div> <button type="submit" className=" mt-3 btn btn-primary float-right"> Create </button> </form> </div> <Toast message={toastMessage} color={toastColor} /> </> ); }; export default NewPost; ``` ### Profile Now we will add the profile page, which will show the user's information. ```jsx= function Profile() { const { userId } = useParams(); const [userData, setUserData] = useState(null); const [posts, setPosts] = useState(null); useEffect(() => { getUserData().then(null); getPosts().then(null) }, []); const getUserData = async () => { await axios.get(`http://localhost:8080/api/user/${userId}`).then( (response)=> { setUserData(response.data.user); }).catch ((error) => { console.error(error); }); }; const getPosts = async() => { await axios.get(`http://localhost:8080/api/post/user/${userId}`).then( (response)=> { setPosts(response.data.posts); }).catch ((error) => { console.error(error); }); } return ( <> { userData && <div className="container mt-3"> <div className="row"> <div className="col-md-4"> <div className="card"> <div className="card-body text-center"> <UserImage email={userData.email}/> <h5 className="card-title">{userData.username}</h5> <p className="card-text text-muted">{userData.intro}</p> <a href={`mailto:${userData.email}`}><i className="fa-regular fa-envelope"></i></a> </div> </div> </div> <div className="col-md-8"> <div className="card"> <div className="card-body"> <ul className="nav nav-tabs" role="tablist"> <li className="nav-item"> <a className="nav-link active" data-bs-toggle="tab" href="#posts" role="tab">Posts</a> </li> <li className="nav-item"> <a className="nav-link" data-bs-toggle="tab" href="#about" role="tab">About</a> </li> </ul> <div className="tab-content"> <div className="tab-pane active" id="posts" role="tabpanel"> <div className="mt-3"> { posts && posts.map((post) => ( <PostSummaryCard post={post} key={post.postId}/> )) } </div> </div> <div className="tab-pane" id="about" role="tabpanel"> <div className="row mt-4"> <div className="col-12"> <p className="card-text">{userData.profile}</p> </div> </div> </div> </div> </div> </div> </div> </div> </div> }</> ); }; export default Profile; ``` ### Edit Profile Finally, we will allow the user to update specific profile information. ```jsx= const EditProfile = () => { const context = useContext(AuthContext); const userId = context.currentUser.userId; //TODO: replace with userId from the context const [firstName, setFirstName] = useState(""); const [middleName, setMiddleName] = useState(""); const [lastName, setLastName] = useState(""); const [username, setUsername] = useState(""); const [email, setEmail] = useState(""); const [mobile, setMobile] = useState(""); const [intro, setIntro] = useState(""); const [profile, setProfile] = useState(""); const headers = { "X-Auth-Token": localStorage.get("authorize") } useEffect(() => { const getUserData = async () => { try { const response = await axios.get(`http://localhost:8080/api/user/${userId}`, {headers: headers}); const user = response.data.user; setFirstName(user.firstName); setMiddleName(user.middleName); setLastName(user.lastName); setUsername(user.username); setEmail(user.email); setMobile(user.mobile); setIntro(user.intro); setProfile(user.profile); } catch (error) { console.log(error); } }; getUserData(); }, []); const handleUpdateProfile = async () => { const data = { firstName, middleName, lastName, email, mobile, intro, profile }; await axios .put(`http://localhost:8080/api/user/${userId}`, data, {headers: headers}) .then((response) => { console.log(response); // handle successful update }) .catch((error) => { console.log(error); // handle error }); }; return ( <div className="card mt-3 "> <div className="card-body"> <div className="row mb-5"> <div className="col-12 text-center "> <UserImage email={email} classes="mx-auto d-block shadow"/><br/> <small className="text-muted ">{username}</small> </div> </div> <div className="row"> <div className="col-md-12"> <div className="row"> <div className="col "> <div className="form-floating"> <input type="text" className="form-control" id="firstName" value={firstName} onChange={(e) => setFirstName(e.target.value)} /> <label htmlFor="firstName" >First Name</label> </div> </div> <div className="col "> <div className=" form-floating"> <input type="text" className="form-control" id="middleName" value={middleName} onChange={(e) => setMiddleName(e.target.value)} /> <label htmlFor="middleName">Middle Name</label> </div> </div> <div className="col "> <div className=" form-floating"> <input type="text" className="form-control" id="lastName" value={lastName} onChange={(e) => setLastName(e.target.value)} /> <label htmlFor="lastName" >Last Name</label> </div> </div> </div> </div> </div> <div className="row mt-2"> <div className="col-md-12"> <div className="row"> <div className="col "> <div className=" form-floating"> <input type="email" className="form-control" id="email" value={email} onChange={(e) => setEmail(e.target.value)} /> <label htmlFor="email" >Email</label> </div> </div> <div className="col "> <div className=" form-floating"> <input type="tel" className="form-control" id="mobile" value={mobile} onChange={(e) => setMobile(e.target.value)} /> <label htmlFor="mobile" >Mobile</label> </div> </div> </div> </div> </div> <div className="row mt-2"> <div className="col-md-12"> <div className="form-floating"> <textarea className="form-control" id="intro" value={intro} onChange={(e) => setIntro(e.target.value)} maxLength="100" style={{ width: "100%", height: "100px", resize:"none" }} // full screen width /><label htmlFor="intro" className="form-label">Intro</label> </div> </div> </div> <div className="row mt-2"> <div className="col-md-12"> <div className="form-floating"> <textarea className="form-control" id="profile" value={profile} onChange={(e) => setProfile(e.target.value)} style={{ width: "100%", height: "350px", resize: "none", overflowY: "auto" }} // full screen width, not resizable, with auto scroll /><label htmlFor="profile" className="form-label">About Me</label> </div> </div> </div> <button className="mt-3 btn btn-primary" onClick={handleUpdateProfile}> Update </button> </div> </div> ); }; export default EditProfile; ``` ### Finish Up Finish up by adding these routes to your App.js file. Remember all these routes are private.