Skip to content

Instantly share code, notes, and snippets.

@dovakeen118
Last active January 30, 2024 18:50
Show Gist options
  • Save dovakeen118/c23cc1240d4e0248c2d7e53b6593f971 to your computer and use it in GitHub Desktop.
Save dovakeen118/c23cc1240d4e0248c2d7e53b6593f971 to your computer and use it in GitHub Desktop.
Updates to Apps generated from Engage

Listed are some updates to make to your app generated with engage. You may make a small branch that includes only these changes so they are easy to track down later.

sass

In the client package.json, remove the dependency node-sass entirely. We will replace it with sass along with updating the version of sass-loader:

"sass-loader": "^7.0.2",
"sass": "^1.22.10",

Anytime we change a package.json we need to re-run yarn install. There are a few more dependency changes in this gist, so feel free to run yarn install at the end

Better Authenticated Routes

Unfortunately, if a user manually refreshes on a page that is rendered by Authenticated Route or if they manually navigate to a path that a Authenticated Route renders components for, even logged in users may get redirected to sign in because the React app hasn't fetched for if the current user is signed in or not.

To fix this, we can add additional logic to AuthenticatedRoute in the client folder, and additional logic to clientRouter in the server folder to better handle paths that require authentication.

import React from "react";
import { Redirect, Route } from "react-router";

const AuthenticationCheck = ({ component: Component, user, ...rest }) => {
  if (user === undefined) {
    return <div>Loading...</div>;
  }
  if (user !== null) {
    return <Component user={user} {...rest} />;
  }
  return <Redirect to="/user-sessions/new" />;
};

const AuthenticatedRoute = ({ component, user, ...rest }) => {
  return (
    <Route
      // eslint-disable-next-line react/jsx-props-no-spreading
      {...rest}
    >
      <AuthenticationCheck user={user} component={component} {...rest} />
    </Route>
  );
};

export default AuthenticatedRoute;

This will check if the user is undefined which is default state for currentUser. If undefined, we render the text "Loading". which then allows the getCurrentUser request to resolve and render the page correctly.

On the backend, we can also add authed paths to the server/routes/clientRouter

import express from "express";
import getClientIndexPath from "../config/getClientIndexPath.js";

const router = new express.Router();

const clientRoutes = ["/", "/user-sessions/new", "/users/new"];

const authedClientRoutes = ["/profile"];

router.get(clientRoutes, (req, res) => {
  res.sendFile(getClientIndexPath());
});

router.get(authedClientRoutes, (req, res) => {
  if (req.user) {
    res.sendFile(getClientIndexPath());
  } else {
    res.redirect("/user-sessions/new")
  }
});

export default router;

You can now deisgnated "authedClientRoutes" that require a user to be logged in. If req.user doesnt exist, then we have no login cookie for the given user, and they are redirected to login!

Update provided User.js jsonSchema Validation

Navigate to the provided User.js model. Looking at the jsonSchema:

Change:

email: { type: "string", format: "email" },

to

email: { type: "string", pattern: "^\\S+@\\S+\\.\\S+$" },

Moving dotenv

Navigate to your /server/package.json. Find the line with "dotenv". Move this out of devDependencies and into dependencies

After the dependency update in the next step, be sure to yarn install

Updating Passport Version

Navigate to /server/package.json

Update:

"passport": "^0.6.0",

to:

"passport": "^0.5.0",

Upon updating, run yarn install again.

Better Error handling in RegistrationForm

Both the RegistrationForm and SignInForm need updates to better handle errors from the backend, and utilizing the front end validations before the POST request is made.

Replace the entire Registration form component with the following:

import React, { useState } from "react";

import config from "../../config";

import ErrorList from "../layout/ErrorList";
import FormError from "../layout/FormError";
import translateServerErrors from "../../services/translateServerErrors";

const RegistrationForm = () => {
  const [userPayload, setUserPayload] = useState({
    email: "",
    password: "",
    passwordConfirmation: "",
  });

  const [errors, setErrors] = useState({});
  const [serverErrors, setServerErrors] = useState({});
  const [shouldRedirect, setShouldRedirect] = useState(false);

  const validateInput = (payload) => {
    setErrors({});
    setServerErrors({});
    const { email, password, passwordConfirmation } = payload;
    const emailRegexp = config.validation.email.regexp.emailRegex;
    let newErrors = {};
    if (!email.match(emailRegexp)) {
      newErrors = {
        ...newErrors,
        email: "is invalid",
      };
    }

    if (password.trim() == "") {
      newErrors = {
        ...newErrors,
        password: "is required",
      };
    }

    if (passwordConfirmation.trim() === "") {
      newErrors = {
        ...newErrors,
        passwordConfirmation: "is required",
      };
    } else {
      if (passwordConfirmation !== password) {
        newErrors = {
          ...newErrors,
          passwordConfirmation: "does not match password",
        };
      }
    }

    setErrors(newErrors);
    if (Object.keys(newErrors).length === 0) {
      return true;
    }
    return false;
  };

  const onSubmit = async (event) => {
    event.preventDefault();

    try {
      if (validateInput(userPayload)) {
        const response = await fetch("/api/v1/users", {
          method: "POST",
          body: JSON.stringify(userPayload),
          headers: new Headers({
            "Content-Type": "application/json",
          }),
        });
        if (!response.ok) {
          if (response.status === 422) {
            const body = await response.json();
            const newServerErrors = translateServerErrors(body.errors);
            return setServerErrors(newServerErrors);
          }
          const errorMessage = `${response.status} (${response.statusText})`;
          const error = new Error(errorMessage);
          throw error;
        }
        return setShouldRedirect(true);
      }
    } catch (err) {
      console.error(`Error in fetch: ${err.message}`);
    }
  };

  const onInputChange = (event) => {
    setUserPayload({
      ...userPayload,
      [event.currentTarget.name]: event.currentTarget.value,
    });
  };

  if (shouldRedirect) {
    location.href = "/";
  }

  return (
    <div className="grid-container">
      <h1>Register</h1>
      <ErrorList errors={serverErrors} />
      <form onSubmit={onSubmit}>
        <div>
          <label>
            Email
            <input type="text" name="email" value={userPayload.email} onChange={onInputChange} />
            <FormError error={errors.email} />
          </label>
        </div>
        <div>
          <label>
            Password
            <input
              type="password"
              name="password"
              value={userPayload.password}
              onChange={onInputChange}
            />
            <FormError error={errors.password} />
          </label>
        </div>
        <div>
          <label>
            Password Confirmation
            <input
              type="password"
              name="passwordConfirmation"
              value={userPayload.passwordConfirmation}
              onChange={onInputChange}
            />
            <FormError error={errors.passwordConfirmation} />
          </label>
        </div>
        <div>
          <input type="submit" className="button" value="Register" />
        </div>
      </form>
    </div>
  );
};

export default RegistrationForm;

Additionally, replace the entire SignInForm component with:

import React, { useState } from "react";

import config from "../../config";

import FormError from "../layout/FormError";

const SignInForm = () => {
  const [userPayload, setUserPayload] = useState({ email: "", password: "" });
  const [shouldRedirect, setShouldRedirect] = useState(false);
  const [errors, setErrors] = useState({});
  const [credentialsErrors, setCredentialsErrors] = useState("");

  const validateInput = (payload) => {
    setErrors({});
    setCredentialsErrors("");
    const { email, password } = payload;
    const emailRegexp = config.validation.email.regexp.emailRegex;
    let newErrors = {};
    if (!email.match(emailRegexp)) {
      newErrors = {
        ...newErrors,
        email: "is invalid",
      };
    }

    if (password.trim() === "") {
      newErrors = {
        ...newErrors,
        password: "is required",
      };
    }

    setErrors(newErrors);
    if (Object.keys(newErrors).length === 0) {
      return true;
    }
    return false;
  };

  const onSubmit = async (event) => {
    event.preventDefault();
    if (validateInput(userPayload)) {
      try {
        const response = await fetch("/api/v1/user-sessions", {
          method: "POST",
          body: JSON.stringify(userPayload),
          headers: new Headers({
            "Content-Type": "application/json",
          }),
        });
        if (!response.ok) {
          if (response.status === 401) {
            const body = await response.json();
            return setCredentialsErrors(body.message);
          }
          const errorMessage = `${response.status} (${response.statusText})`;
          const error = new Error(errorMessage);
          throw error;
        }
        const userData = await response.json();
        setShouldRedirect(true);
      } catch (err) {
        console.error(`Error in fetch: ${err.message}`);
      }
    }
  };

  const onInputChange = (event) => {
    setUserPayload({
      ...userPayload,
      [event.currentTarget.name]: event.currentTarget.value,
    });
  };

  if (shouldRedirect) {
    location.href = "/";
  }

  return (
    <div className="grid-container" onSubmit={onSubmit}>
      <h1>Sign In</h1>

      {credentialsErrors ? <p className="callout alert">{credentialsErrors}</p> : null}

      <form>
        <div>
          <label>
            Email
            <input type="text" name="email" value={userPayload.email} onChange={onInputChange} />
            <FormError error={errors.email} />
          </label>
        </div>
        <div>
          <label>
            Password
            <input
              type="password"
              name="password"
              value={userPayload.password}
              onChange={onInputChange}
            />
            <FormError error={errors.password} />
          </label>
        </div>
        <div>
          <input type="submit" className="button" value="Sign In" />
        </div>
      </form>
    </div>
  );
};

export default SignInForm;

In the back-end, replace the usersRouter.js to account for error handling

import express from "express";
import { ValidationError } from "objection";

import { User } from "../../../models/index.js";

const usersRouter = new express.Router();

usersRouter.post("/", async (req, res) => {
  const { email, password } = req.body;
  try {
    const persistedUser = await User.query().insertAndFetch({ email, password });
    return req.login(persistedUser, () => {
      return res.status(201).json({ user: persistedUser });
    });
  } catch (error) {
    if (error instanceof ValidationError) {
      return res.status(422).json({ errors: error.data });
    }
    return res.status(500).json({ error: errors.message });
  }
});

export default usersRouter;

And lastly, replace the usersSessionRouter.js:

import express from "express";
import passport from "passport";

const sessionRouter = new express.Router();

sessionRouter.post("/", (req, res, next) => {
  return passport.authenticate("local", (err, user) => {
    if (err) {
      // eslint-disable-next-line no-console
      console.log(err);
    }

    if (user) {
      return req.login(user, () => {
        return res.status(201).json(user);
      });
    }

    return res.status(401).json({
      message:
        "Either email or password are incorrect. Please try again, or Sign Up to create a new account.",
    });
  })(req, res, next);
});

sessionRouter.get("/current", async (req, res) => {
  if (req.user) {
    res.status(200).json(req.user);
  } else {
    res.status(401).json(undefined);
  }
});

sessionRouter.delete("/", (req, res) => {
  req.logout();
  res.status(200).json({ message: "User signed out" });
});

export default sessionRouter;

Resolve Warnings

Some warnings pop up in the terminal (creating unnecessary noise) that can be resolved with the following changes.

Babel

In the client/babel.config.js look for the following around line 37:

      [
        require("@babel/plugin-proposal-class-properties").default,
        {
          loose: true,
        },
      ],

Remove this plugin and replace with the following line:

require("@babel/plugin-transform-class-properties").default,

Foundation Settings

The settings for Foundation live directly in our app and need some updates to avoid warnings when the app is running an evaluating the CSS.

In the client/src/assets/scss/foundation/_settings.scss around line 464:

$input-padding: $form-spacing / 2;
// ^^ remove this line

$input-padding: calc($form-spacing / 2);
// replace with ^^

Lastly, in the same file at line 810:

$table-head-background: smart-scale($table-background, $table-color-scale / 2);
// ^^ remove this line

$table-head-background: smart-scale($table-background, calc($table-color-scale / 2));
// replace with ^^

CSS doesn't like using straight division to calculate a value, and instead wants the method calc to be used now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment