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.
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
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!
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+$" },
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
Navigate to /server/package.json
Update:
"passport": "^0.6.0",
to:
"passport": "^0.5.0",
Upon updating, run yarn install
again.
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;
Some warnings pop up in the terminal (creating unnecessary noise) that can be resolved with the following changes.
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,
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.