React User Authentication

This month has been crazy learning about a TON of new and different “user security” topics. It was very overwhelming at times. Here are all of the different aspects of the React side for users which I learned and developed:

  • Fetching Secure Data from Django
  • Enabling User Login
  • Automatic Token Refresh
  • User Logout
  • Registration
  • Protected Routes
  • User Menu Changes
  • Converting the Class Components to Functional Components
  • Environment Settings

Fetching Secure Data from Django

Fetching Data from Django was no easy task. Originally, all I needed was a basic Axios get (or put or delete). But when you conditionally incorporate tokens into the headers, things can get complex.

Conditionally: only when the user is authenticated (and how to track that) should the token be placed into the header. In my environment, upon login, the code was able to properly dynamically add the token to the header. Upon the next component load, despite the token fully loaded in a cookie and local storage, the code would always skip the entire Axios request and response interceptor. So, I had to bail on using Axios (it’s more important to have the application working than a certain library, right?!)

The basic JavaScript fetch is what I was able to get to correctly conditionally add the authorization clause to the headers.

Enabling User Login

At CalPERS, due to our tiny custom framework, we didn’t tend to use publicly available modules to do our work: everything was custom-built. At first, I went down the path of “No problem: I can write all of the user authentication states and tokens myself”. Two days later, I gave up and started searching for JavaScript modules that could take care of User Authentication state management instead of me. I used the react-auth-kit to manage the user state. I hoped that this kit would become a simple catch-all for everything user authentication, but there were more items that I would have to write myself.

I decided to have both access and refresh tokens to be saved in local storage so that they could be used across multiple browser tabs and windows. Oftentimes, a user (like me) will go back and forth between searching existing postings and adding new ones while sometimes also having a weekly status report open.

I also chose to store the refresh token in the HTTP cookie managed by react-auth-kit. Since I don’t have certificates set up locally, I set cookieSecure to false. For a typical production app, I would know better and always have HTTPS enabled and only use secured cookies.

My other key workaround from the above issue: traffic to the back end only uses Axios to access the login endpoint (everywhere secure uses fetch).

// Login.jsx
import React, { useState } from "react";
import { useNavigate } from "react-router-dom";
import { useSignIn } from "react-auth-kit";  // Correct hook to handle login
import axios from "axios";
import { Container, Form, Card, CardTitle, CardBody, CardFooter, FormGroup, Label, Input } from "reactstrap";

const Login = () => {
  const [username, setUserName] = useState("");
  const [password, setPassword] = useState("");
  const [error, setError] = useState(""); // Error handling state
  const navigate = useNavigate(); // Hook to navigate after login
  const signIn = useSignIn(); // Use useSignIn hook to handle login

  const handleLogin = async (e) => {
    e.preventDefault();

    try {
      // Make the POST request to your Django login API (replace URL with your Django endpoint)
      const apiUrl = process.env.REACT_APP_API_URL;
      if (!apiUrl) {
        throw new Error("API URL is not defined in environment variables");
      }

      const response = await axios.post(`${apiUrl}auth/login/`, { username, password });

      // Assuming the response contains the token and user info
      const { access, refresh, user } = response.data;

      // Store the access token in LocalStorage
      localStorage.setItem("access_token", access);
      localStorage.setItem("refresh_token", refresh);

      // Call signIn from react-auth-kit
      const isSignedIn = signIn({
        token: refresh,
        expiresIn: 3600, // Token expiration time in seconds
        tokenType: "Bearer",
        authState: user, // Save user info
        sameSite: 'None',       // This allows the cookie to be sent in cross-origin requests
      });

      if (!isSignedIn) {
        throw new Error("Failed to sign in. Please try again.");
      }

      navigate("/dashboard");  // Redirect to the protected page after successful login

    } catch (error) {
      // Check if the error response contains a message to display
      if (error.response && error.response.data.error && error.response.data.detail) {
        setError(error.response.data.detail); // Display backend error message
      } else {
        setError(error.message || "An error occurred. Please try again.");
      }
    }
  };

  return (
    <Container className="centered-container">
      <Form onSubmit={handleLogin}>
        <Card className="text-dark bg-light m-3 card-narrow">
          <CardTitle><strong>Login</strong></CardTitle>
          <CardBody>
            <FormGroup>
              <Label>User Name</Label>
              <Input
                type="text"
                value={username}
                onChange={(e) => setUserName(e.target.value)}
              />
            </FormGroup>
            <FormGroup>
              <Label>Password</Label>
              <Input
                type="password"
                value={password}
                onChange={(e) => setPassword(e.target.value)}
              />
            </FormGroup>
            {error && <p style={{ color: "red" }}>{error}</p>} {/* Show error message */}
          </CardBody>
          <CardFooter>
            <button type="submit">Login</button>
          </CardFooter>
        </Card>
      </Form>
    </Container>
  );
};

export default Login;

The react-auth-kit was used in four places: Login, Logout, checking protected routes, and the index.js AuthProvider. Here is how I implemented the AuthProvider code:

// index.js
import React, { StrictMode } from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import { AuthProvider } from 'react-auth-kit';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));

root.render(
  <AuthProvider authType="cookie" authName="_auth" 
                cookieDomain={window.location.hostname} cookieSecure={false}>
    <BrowserRouter>
      <StrictMode>
        <App />
      </StrictMode>
    </BrowserRouter>
  </AuthProvider>
);

Automatic Token Refresh

Conceptually, “get a token, store the token, refresh the token” is rather simple. But to implement it in react, coupled with the react component double render (send traffic to the back end twice), quickly became problematic. One of the benefits of JavaScript and React is its ability to do multi-threading. Unfortunately, you can’t multi-thread traffic to the back end when you are refreshing the token. You’ll kick off the first call to the back end then await it to come back. Meanwhile, another post is kicked off to the back end. While the first refreshed token is coming back, a second one is requested and… that loop would be enough to drive most…

The variable isRefreshing is used to prevent duplicate refresh attempts. Without isRefreshing, multiple 401 responses from concurrent requests would trigger multiple calls to refreshAccessToken. This caused issues with the refresh endpoint. The isRefreshing flag acts as a single-entry guard to ensure only one token refresh process is started.

The code uses the refreshPromise to ensure that all requests share the same promise. If one request triggers a token refresh, other concurrent requests that encounter 401 responses wait on refreshPromise instead of starting new refresh attempts. This avoids redundant network calls and ensures that all requests get the same updated token. The promise tracks the ongoing refresh operation and allows all dependent requests to respond appropriately based on the result.

The combination in action:

  1. First 401 response:
    • isRefreshing is false, so the code sets isRefreshing = true and starts refreshPromise = refreshAccessToken().
  2. Subsequent 401 responses during the same refresh:
    • isRefreshing is true, so they wait on refreshPromise.
  3. Once the refresh completes:
    • The refreshPromise resolves or rejects.
    • All waiting requests proceed with the new token or handle the failure.
    • isRefreshing and refreshPromise are reset in the finally block.

Why this design?

The design ensures efficiency, prevents redundant refresh attempts, and allows smooth handling of concurrent requests needing a refreshed token. Both variables serve complementary purposes:

  • isRefreshing acts as a quick flag to prevent initiating multiple refreshes.
  • refreshPromise is the actual mechanism to share the result of the ongoing refresh among all waiting requests.
//customFetch.js
import { TOKEN_REFRESH_API_URL } from "./constants";

export async function customFetch(url, options = {}, accessToken, refreshToken, navigate) {
  let isRefreshing = false; // Track if the token refresh process is ongoing
  let refreshPromise = null; // To store the promise of the ongoing refresh process

  try {
    // Helper function to refresh the token
    async function refreshAccessToken() {
      const response = await fetch(TOKEN_REFRESH_API_URL, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ refresh: refreshToken }),
      });

      if (!response.ok) {
        // If refreshing fails, navigate to login
        navigate('/login');
        throw new Error('Failed to refresh access token');
      }

      const data = await response.json();
      return data.access; // The new access token
    }

    // Set up headers with the access token
    const headers = {
      'Content-Type': 'application/json',
      ...(accessToken && { Authorization: `Bearer ${accessToken}` }),
    };

    // Ensure the body is correctly included for POST/PUT methods
    const body =
      (options.method === 'POST' || options.method === 'PUT' || options.method === 'DELETE')
        ? JSON.stringify(options.body)
        : null;

    let response = await fetch(url, {
      ...options,
      headers,
      body, // Add the body to the request
    });

    if (response.status === 401 && refreshToken && !isRefreshing) {
      // Access token expired, start the refresh process
      isRefreshing = true;
      console.log('Access token expired, attempting to refresh...');

      // Start refreshing the access token only once
      refreshPromise = refreshAccessToken();

      // Wait for the refresh to complete and get the new token
      const newAccessToken = await refreshPromise;

      // Save the new access token to localStorage
      localStorage.setItem('access_token', newAccessToken);

      // Now retry the original request with the new access token
      const retryHeaders = {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${newAccessToken}`,
      };

      response = await fetch(url, {
        ...options,
        headers: retryHeaders,
        body, // Retry the body if needed
      });

      if (!response.ok) {
        throw new Error(`HTTP Error: ${response.status} - ${response.statusText}`);
      }

      // Parse and return the JSON data from the retried response
      const retryData = await response.json();
      return retryData || [];
    }

    // If no refresh needed, handle the normal response
    if (!response.ok && response.status !== 401) {
      console.error(`HTTP Error: ${response.status} - ${response.statusText}`);
    }

    // Handle empty or non-JSON response body
    const contentType = response.headers.get('Content-Type');
    if (!contentType || !contentType.includes('application/json')) {
      const text = await response.text();
      return text.trim().length === 0 ? [] : [];
    }

    // Parse and return JSON data
    const data = await response.json();
    return data || [];
  } catch (error) {
    console.error('Custom fetch error:', error);
    return [];
  } finally {
    // Reset the refresh flag once the refresh process is done
    isRefreshing = false;
    refreshPromise = null;
  }
}

Wrapping customFetch with useApiRequest creates a more structured and reusable API interaction mechanism tailored for use within react components. It enables:

  • Centralized Error Handling – The useApiRequest hook captures errors (setError) in a state variable that can be easily accessed by React components.
  • React State Integration – useState: Manages the error state, which is tied to the React component lifecycle. This enables re-renders and seamless error updates in the UI.
  • Memoization for Stable References – useCallback: Ensures that apiRequest has a stable reference, avoiding unnecessary re-creations and improving performance, especially when passed as a dependency to other hooks (e.g., useEffect) or components. React re-renders only when one of the dependencies (navigate, accessToken, refreshToken) changes.
  • Simplified and Reusable API Calls – The hook abstracts away repetitive setup
// useApiRequest.js
import { useNavigate } from 'react-router-dom'; // To redirect to login on failure
import { useState, useCallback } from 'react'; // Import useCallback
import { customFetch } from './customFetch'; // import customFetch

export function useApiRequest() {
  const navigate = useNavigate();
  const [error, setError] = useState(null);
  const accessToken = localStorage.getItem("access_token");
  const refreshToken = localStorage.getItem("refresh_token");

  // Use useCallback to memoize apiRequest
  const apiRequest = useCallback(async (url, body = {}, options = {}) => {
    try {
      const response = await customFetch(url, { ...options, body }, accessToken, refreshToken, navigate);
      return response; // Return the response for use in components
    } catch (err) {
      setError(err.message); // Set error in case of failure
      console.error(err);
      return null;
    }
  }, [navigate, accessToken, refreshToken]); // Add these as dependencies to ensure stable memoization

  return { apiRequest, error };
}

User Logout

The logout does all of the cleanup:

  • Removes the localStorage entries
  • Calls the back end, which invalidates the tokens
  • Calls the react-auth-kit signOut which, among other things, removes the saved cookie

Unlike the login which only needs to be accessible from its own page, logout needs to be available everywhere. The function was placed in the app.js component itself.

  const handleLogout = async () => {
    // Make the logout API call to invalidate the token
    await apiRequest('/api/logout/', {}, { method: 'POST' }); // Adjust the endpoint if needed
    signOut();  // Sign out the user
    localStorage.removeItem("access_token");

    navigate("/");// Redirect to home page after logout
  };

Registration

The Register component has regular expression validations for the username, password, and email. The error reporting has two parts:

  • Front-end error handling, showing up right next to the field itself
  • Back-end error handling, which could be one or many rows, even just associated with one field. Django is the first framework that I have worked with where I was able to use advanced validators. It is the Django output that is displayed at the bottom of the component, right above the footer itself.

If I had extra time, I would have integrated the react-hook-form in these new functional components, which I wasn’t using before (they used to be class components). You’ll also notice that the code sources the react env file to determine whether to enforce strong passwords or not. And, one of my favorite features on other sites: show and hide the password. This way, the user can choose to see the password that they entered.

import React, { useState } from "react";
import { useNavigate } from "react-router-dom";
import axios from "axios";
import { Container, Form, Card, CardTitle, CardBody, CardFooter, Row, Col, FormGroup, Label, Input } from "reactstrap";

import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faEye, faEyeSlash } from "@fortawesome/free-solid-svg-icons";

const ENFORCE_STRONG_PASSWORD = process.env.REACT_APP_ENFORCE_STRONG_PASSWORD === "true";

export default function Register() {
    const [username, setUserName] = useState("");
    const [password, setPassword] = useState("");
    const [firstName, setFirstName] = useState("");
    const [lastName, setLastName] = useState("");
    const [email, setEmail] = useState("");
    const [showPassword, setShowPassword] = useState(false); 
    const [passwordError, setPasswordError] = useState("");
    const [usernameError, setUsernameError] = useState("");
    const [emailError, setEmailError] = useState("");
    const [backendErrors, setBackendErrors] = useState([]);
    const navigate = useNavigate();

    const handlePasswordToggle = () => {
        setShowPassword((prevState) => !prevState);
    };

    const validateUserName = (username) => {
        const usernameRegex = /^(?=.{3,20}$)(?![_\-.])(?!.*[_\-.]{2})[a-zA-Z0-9._-]+(?<![_\-.])$/;

        if (!usernameRegex.test(username)) {
            setUsernameError(
                "Username must be 3–20 characters long, alphanumeric, and can include underscores (_), dots (.), or hyphens (-). It cannot start, end, or have consecutive special characters."
            );
            return false;
        }

        setUsernameError(""); // Clear error if validation passes
        return true;
    }

    const validatePassword = (password) => {
        if (!ENFORCE_STRONG_PASSWORD) {
            return true; // No validation if the feature is disabled
        }

        const strongPasswordRegex =
            /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*(),.?":{}|<>])[A-Za-z\d!@#$%^&*(),.?":{}|<>]{12,}$/;


        if (!strongPasswordRegex.test(password)) {
            setPasswordError(
                "Password must be at least 12 characters long, include at least one uppercase letter, one lowercase letter, one number, and one special character."
            );
            return false;
        }

        setPasswordError(""); // Clear error if validation passes
        return true;
    };

    const validateEmail = (email) => {
        const sanitizedEmail = email.trim(); // Trim whitespace
        const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,20}$/;

        if (!emailRegex.test(sanitizedEmail)) {
            setEmailError(
                "Please enter a valid email address."
            );
            console.log("Email Address Tested: " + email)
            return false;
        }

        setEmailError(""); // Clear error if validation passes
        return true;
    }

    const handleSubmit = async (e) => {
        e.preventDefault();

        // Run all validations before proceeding
        const isUsernameValid = validateUserName(username);
        const isPasswordValid = validatePassword(password);
        const isEmailValid = validateEmail(email);

        // If any validation fails, stop form submission
        if (!isUsernameValid || !isPasswordValid || !isEmailValid) {
            console.error("Validation failed. Please fix the errors and try again.");
            return;
        }

        try {
            const apiUrl = process.env.REACT_APP_API_URL;
            const response = await axios.post(`${apiUrl}auth/register/`, {
                username,
                password,
                first_name: firstName,
                last_name: lastName,
                email,
            });

            if (response.status === 201) { // Assuming 201 Created for successful registration
                console.log("Registration successful:", response.data);
                navigate("/login"); // Redirect to login page after successful registration
            } else {
                console.error("Unexpected response during registration:", response);
            }
        } catch (err) {
            console.error("Error during registration:", err);
            console.error(err.response?.data?.error)
            setBackendErrors(
                Array.isArray(err.response?.data?.error)
                  ? err.response.data.error
                  : typeof err.response?.data?.error === 'string'
                  ? err.response?.data?.error.startsWith('[')  // Check if the string looks like an array
                    ? JSON.parse(err.response?.data?.error.replace(/'/g, '"'))  // Parse the string as JSON if it's an array-like string
                    : [err.response?.data?.error]  // Otherwise, wrap the single error in an array
                  : ['An unknown error occurred']
              );
              
        }
    }

    return (
        <Container className="centered-container">
            <Form onSubmit={handleSubmit}>
                <Card className="text-dark bg-light m-3 card-medium">
                    <CardTitle><strong>Register</strong></CardTitle>
                    <CardBody className="bg-white">
                        <Row id="full_name" >
                            <Col id="first_name" >
                                <FormGroup>
                                    <Label>First Name</Label>
                                    <Input
                                    type="text" required
                                    value={firstName}
                                    onChange={(e) => setFirstName(e.target.value)}
                                    />
                                </FormGroup>
                            </Col>
                            <Col id="last_name" >
                                <FormGroup>
                                    <Label>Last Name</Label>
                                    <Input
                                    type="text" required
                                    value={lastName}
                                    onChange={(e) => setLastName(e.target.value)}
                                    />
                                </FormGroup>
                            </Col>
                        </Row>
                        <Row id="user_pass" >
                            <Col id="user_name" >
                                <FormGroup>
                                    <Label>User Name</Label>
                                    <Input
                                    type="text" required
                                    value={username}
                                    onChange={(e) => setUserName(e.target.value)}
                                    onBlur={() => validateUserName(username)}
                                    />
                                    {usernameError && (
                                        <div className="text-danger mt-1">{usernameError}</div>
                                    )}
                                </FormGroup>
                            </Col>
                            <Col id="pass" >
                                <FormGroup>
                                    <Label>Password</Label>
                                    <div className="input-group">
                                        <Input
                                            type={showPassword ? "text" : "password"}
                                            value={password}
                                            onChange={(e) => setPassword(e.target.value)}
                                            onBlur={() => validatePassword(password)}
                                        />
                                        <span
                                            className="input-group-text"
                                            onClick={handlePasswordToggle}
                                            style={{ cursor: "pointer" }}
                                        >
                                            <FontAwesomeIcon
                                                icon={showPassword ? faEyeSlash : faEye}
                                            />
                                        </span>
                                    </div>
                                    {passwordError && (
                                        <div className="text-danger mt-1">{passwordError}</div>
                                    )}
                                </FormGroup>
                            </Col>
                        </Row>
                        <Row>
                            <Col>
                                <FormGroup>
                                    <Label>Email Address</Label>
                                    <Input
                                        type="email"
                                        value={email}
                                        onChange={(e) => setEmail(e.target.value)}
                                        onBlur={() => validateEmail(email)}
                                    />
                                    {emailError && (
                                        <div className="text-danger mt-1">{emailError}</div>
                                    )}
                                </FormGroup>
                            </Col>
                        </Row>

                        {backendErrors.length > 0 && (
                            <Row className="error-summary alert alert-danger mb-4">
                                <Col>
                                    <h5>Please correct the following:</h5>
                                    <ul>
                                    {backendErrors.map((error, index) => (
                                        <li key={index}>{error}</li>
                                    ))}
                                    </ul>
                                </Col>
                            </Row>
                        )}

                    </CardBody>
                    <CardFooter>
                        <button type="submit">Register</button>
                    </CardFooter>
                </Card>
            </Form>
        </Container>
    )
}

Protected Routes

When a user tries to navigate to any given react route, the routes that require authentication need to check whether the user is correctly logged in or not. The route protection uses the react-auth-kit to ensure that the user is authenticated.

import React from 'react';
import { Navigate } from 'react-router-dom';
import { useIsAuthenticated } from 'react-auth-kit';

const ProtectedRoute = ({ element, ...rest }) => {
  const isAuthenticated = useIsAuthenticated();

  // If the user is not authenticated, redirect to the login page
  if (!isAuthenticated()) {
    return <Navigate to="/login" />;
  }

  // If authenticated, render the passed-in element
  return element;
};

export default ProtectedRoute;

User Menu Changes

I think that the most fun I had with this particular release was the changes to the navigation bar and router. The navigation bar and router render differently based on authentication using the react-auth-kit isAuthenticated.

When a user is not authenticated:

  • the nav bar only has Links to all of the static documents with no Information sub-menu.
  • The far right end of the nav bar has the Login and Register elements
  • The root is routed to the Welcome component.

When a user is authenticated:

  • All of the documentation content is in the Information sub-menu.
  • There is no ‘Welcome’ component: the Dashboard component exists instead
  • The far right end of the nav bar has the user name with the sub-menu for logout (and other eventual user functions)
  • The root is routed to the Dashboard component.

Here is how my final app.js code became:

    <div>
      <Navbar color="light" light expand="md">
        <NavbarBrand href="/">Job Search Tracker</NavbarBrand>
        <NavbarToggler onClick={toggle} />
        <Collapse isOpen={isOpen} navbar>
        
        {isAuthenticated() ? (
          <Nav className="ml-auto" navbar>
            <NavItem>
              <NavLink href="/dashboard">Dashboard</NavLink>
            </NavItem>
            {/* Static Pages Submenu */}
            <UncontrolledDropdown nav inNavbar>
              <DropdownToggle nav caret>
                Information
              </DropdownToggle>
              <DropdownMenu end>
                <DropdownItem tag={Link} to="/about">About</DropdownItem>
                <DropdownItem tag={Link} to="/job-hunt-tips">Job Hunt Tips</DropdownItem>
                <DropdownItem tag={Link} to="/boolean-search">Boolean Search</DropdownItem>
                <DropdownItem tag={Link} to="/financial-assistance">Financial Assistance Programs</DropdownItem>
                <DropdownItem divider />
                <DropdownItem tag={Link} to="/release-history">Release History</DropdownItem>
              </DropdownMenu>
            </UncontrolledDropdown>
            <NavItem>
              <NavLink href="/job-sites">Job Sites</NavLink>
            </NavItem>
            <NavItem>
              <NavLink href="/job-postings">Job Postings</NavLink>
            </NavItem>
            <NavItem>
              <NavLink href="/opportunities">Opportunities</NavLink>
            </NavItem>

            {/* Reports Submenu */}
            <UncontrolledDropdown nav inNavbar>
              <DropdownToggle nav caret>
                Reports
              </DropdownToggle>
              <DropdownMenu end>
                <DropdownItem tag={Link} to="/reports/postingsAppliedSince">Postings Applied</DropdownItem>
                <DropdownItem tag={Link} to="/reports/perSite">Per Site</DropdownItem>
                <DropdownItem tag={Link} to="/reports/perWeek">Per Week</DropdownItem>
              </DropdownMenu>
            </UncontrolledDropdown>
          </Nav>
        ) : (
          <Nav className="ml-auto" navbar>
            <NavItem><NavLink href="/">Welcome</NavLink></NavItem>
            <NavItem><NavLink href="/about">About</NavLink></NavItem>
            <NavItem><NavLink href="/job-hunt-tips">Job Hunt Tips</NavLink></NavItem>
            <NavItem><NavLink href="/boolean-search">Boolean Search</NavLink></NavItem>
            <NavItem><NavLink href="/financial-assistance">Financial Assistance Programs</NavLink></NavItem>
            <NavItem><NavLink href="/release-history">Release History</NavLink></NavItem>
          </Nav>
        )}
        

        <Nav className="ms-auto" navbar>
        {isAuthenticated() ? (
          <UncontrolledDropdown nav inNavbar>
            <DropdownToggle nav caret>
              Hi {user()?.first_name || 'First Name Not Available'}!
            </DropdownToggle>
            <DropdownMenu end>
              <DropdownItem tag={Link} to="/edit-profile">Edit Profile</DropdownItem>
              <DropdownItem divider />
              <DropdownItem onClick={handleLogout}>Logout</DropdownItem>
            </DropdownMenu>
          </UncontrolledDropdown>
        ) : (
          <>
            <NavItem>
              <NavLink tag={Link} to="/login">Login</NavLink>
            </NavItem>
            <NavItem>
              <NavLink tag={Link} to="/register">Register</NavLink>
            </NavItem>
          </>
        )}
        </Nav>
        </Collapse>
      </Navbar>

      <Routes>
        {/* Conditional Routing for / based on Authentication */}
        <Route path="/" element={isAuthenticated() ? <Dashboard /> : <Welcome />} />

        {/* Public Routes */}
        <Route path="/welcome" element={<Welcome />} />
        <Route path="/login" element={<Login />} />
        <Route path="/register" element={<Register />} />
        <Route path="/about" element={<About />} />
        <Route path="/job-hunt-tips" element={<JobHuntTips />} />
        <Route path="/release-history" element={<ReleaseHistory />} />
        <Route path="/financial-assistance" element={<FinancialAssistance />} />
        <Route path="/boolean-search" element={<BooleanSearch />} />

        {/* Protected Routes */}
        <Route path="/secret" element={<ProtectedRoute element={<Secret />} />} />
        <Route path="/dashboard" element={<ProtectedRoute element={<Dashboard />} />} />
        <Route path="/job-sites" element={<ProtectedRoute element={<JobSiteList />} />} />
        <Route path="/job-site-view/:id" element={<ProtectedRoute element={<JobSiteView />} />} />
        <Route path="/job-site-new" element={<ProtectedRoute element={<JobSiteEdit />} />} />
        <Route path="/job-site-new/:id" element={<ProtectedRoute element={<JobSiteEdit />} />} />
        <Route path="/job-site-edit" element={<ProtectedRoute element={<JobSiteEdit />} />} />
        <Route path="/job-site-edit/:id" element={<ProtectedRoute element={<JobSiteEdit />} />} />
        <Route path="/job-postings" element={<ProtectedRoute element={<JobPostingList />} />} />
        <Route path="/job-posting-new/:id" element={<ProtectedRoute element={<JobPostingEdit />} />} />
        <Route path="/job-posting-edit/:id" element={<ProtectedRoute element={<JobPostingEdit />} />} />
        <Route path="/opportunities" element={<ProtectedRoute element={<OpportunityList />} />} />
        <Route path="/opportunity-details" element={<ProtectedRoute element={<OpportunityDetails />} />} />
        <Route path="/opportunity-details/:id" element={<ProtectedRoute element={<OpportunityDetails />} />} />
        <Route path="/reports" element={<ProtectedRoute element={<Reports />} />} />
        <Route path="/reports/:reportType/:referenceDate?" element={<ProtectedRoute element={<Reports />} />} />
      </Routes>

Converting the Class Components to Functional Components

When you are first learning to develop React online, you see two different kinds of React components out there. At first glance, honestly, class components were initially easier to learn than functional. React doesn’t typically live by itself: there is a back end. So many of the back-end references of “build an application using X MVC and React” (Laravel with React, Rails with React, Django with React) are written using class components instead of the modern functional components. That is what I learned and it just worked, so I stayed with my class components.

Later, though all of my complex components with forms were still using the class definition, I started writing the newer static pages (no back-end server data) as functional components. One of the imported components ( the editor component) was already a functional component.

The modern functional components enable new features like the useEffect, memoization, and other features that are necessary for the fetch to properly work and for the back-end data-intensive components to properly render the data. So, I converted every single one of my components over to the modern functional component structure.

Environment Settings

Getting closer to deploying the application as a container to run in AWS, you start thinking about the differences between development and production environments. Having to create a few new constants, it was time to start using the correct import structure.

Static constants are all stored in (for now):

// frontend/src/constants/index.js
const BASE_API_URL = process.env.REACT_APP_API_URL;

export const JOB_OPPORTUNITY_API_URL = BASE_API_URL + 'email_opportunity/';
export const JOB_SITE_API_URL = BASE_API_URL + 'job_site/';
export const JOB_POSTING_API_URL = BASE_API_URL + 'job_posting/';
export const DASHBOARD_API_URL = BASE_API_URL + 'dashboard/';
export const REPORT_API_URL = BASE_API_URL + 'report/';
export const TOKEN_REFRESH_API_URL = BASE_API_URL + 'token/refresh/'; 

export const formatInputFieldDateTime = (originalDateTime) => {
    if (originalDateTime === null) return null;
    // Properly format date-time
    // Going into the database, the time zone is saved.
    // The front end widget does not need a time zone, nor the 'T'.
    // From: 2024-09-02T14:19:00-07:00
    // To: 2024-09-02 14:19:00   (no time zones)

    var originalDtArr = originalDateTime.split('T')
    var originalDtTimeArr = originalDtArr[1].split('-')
    return originalDtArr[0] + ' ' + originalDtTimeArr[0]
}

export const formatDisplayDateTime = (rawDate) => {
    if (rawDate === null) return null;
    // From a code perspective, I would love to live with the default:
    // const theDate = new Date(rawDate).toLocaleString('en-US')
    // 9/5/2024, 5:42:00 PM
    // but... you can't easily turn off the seconds!
    // return: Tuesday, Sep 10, 2024, 5:42 AM
    const theDate = new Date(rawDate).toLocaleString('en-US', {
        weekday: 'long',
        year: 'numeric',
        month: 'short',
        day: 'numeric',
        hour12:true,
        hour:'numeric',
        minute:'numeric'})
    return theDate
}

// return: Sep 10, 2024
export const formatDisplayDate = (rawDate) => {
    if (rawDate === undefined || rawDate === null) return null;
    
    const theDate = new Date(rawDate).toLocaleString('en-US', {
        year: 'numeric',
        month: 'short',
        day: 'numeric'})
    return theDate
}

Yes, those utility functions really belong in something separate from the src/constants/index.js file but that is to be fixed at another time. The constants file imports the process.env values, which are defined as:

/.env
REACT_APP_API_URL=http://localhost:8000/api/
REACT_APP_ENABLE_AUTH=true
REACT_APP_ENV=development  # This can be 'development' or 'production'

This is the file that I can either manually change or have docker adjust for setting which environment to use.

Final Thoughts

This sprint has been the most challenging yet. However, I persevered, learning many new react features and the reasons why, long term, all components should be implemented with functional components. It has enabled me to have multiple users, so that my test data doesn’t mingle with my actual job hunt data and can enable me to have a third sample user data. This release really readies the code for a truly production-worthy application.