Skip to content

Commit

Permalink
completed app and added comments
Browse files Browse the repository at this point in the history
  • Loading branch information
Daniel-Workman committed Jul 31, 2022
1 parent 81caa5a commit 3de0089
Show file tree
Hide file tree
Showing 4 changed files with 41 additions and 24 deletions.
9 changes: 3 additions & 6 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,17 @@ import Home from "./components/Home/Home";
import Cookies from "universal-cookie";

function App() {
//initalize cookies to then pass to children components
const cookies = new Cookies();
//initalize some state to pass to children components to control access to the application
const [userSignedIn, setUserSignedIn] = useState(false);

return (
<>
<Routes>
<Route
path="/"
element={
<AuthForm cookies={cookies} setUserSignedIn={setUserSignedIn} />
}
/>
<Route
path="/home"
//use state to control if access is allowed on the home route to the homepage or auth screen
element={
userSignedIn ? (
<Home cookies={cookies} setUserSignedIn={setUserSignedIn} />
Expand Down
18 changes: 7 additions & 11 deletions client/src/components/AuthForm/AuthForm.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import React, { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { Visibility, VisibilityOff } from "@material-ui/icons/";
import { Input, Button, InputAdornment, IconButton } from "@material-ui/core/";
import axios from "axios";

function AuthForm(props: any) {
//grab the functions from the parent component via props
const { setUserSignedIn, cookies } = props;
const navigate = useNavigate();
//set the initial state for the auth form to be signin
const [isSignup, setIsSignup] = useState(false);
//create a new set of data for the form and set the initial state as the empty fields above
Expand Down Expand Up @@ -35,13 +34,10 @@ function AuthForm(props: any) {
axios
.post("http:https://localhost:5001/user/login-user", formData)
.then((response) => {
// console.log(response);
// let data = response.data;
const rToken = response.data.newRefreshTkn;
// console.log(rToken);
// setTokens(data);
//set the JWT token from the response in browser cookies
cookies.set("refreshToken", rToken, { path: "/" });
// console.log(cookies.get("refreshToken"));
//update parent state to control visibility on the homepage component
setUserSignedIn(true);
})
.catch((error) => {
Expand All @@ -52,16 +48,16 @@ function AuthForm(props: any) {
axios
.post("http:https://localhost:5001/user/create-user", formData)
.then((response) => {
console.log(response);
// let data = response.data;
// setTokens(data);
const rToken = response.data.newRefreshTkn;
//set the JWT token from the response in browser cookies
cookies.set("refreshToken", rToken, { path: "/" });
//update parent state to control visibility on the homepage component
setUserSignedIn(true);
})
.catch((error) => {
console.log(error);
});
}
navigate("/home");
};

//when any input field in the formData state is updated it will take a copy of the initial state
Expand Down
5 changes: 5 additions & 0 deletions client/src/components/Home/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,18 @@ import AuthForm from "../AuthForm/AuthForm";
import axios from "axios";

function Home(props: any) {
//grab the functions from parent component via props
const { cookies, setUserSignedIn } = props;
//grab the refresh token from the browser which was set during auth.
let refreshTkn = cookies.get("refreshToken");

//useRef to stop the useEffect from firing on the initial render.
//This is because on initial load the token has just been set and we only need to send a request to update
//on the next re-render of the component however it may be triggered.
const didMount = useRef(false);

//useEffect to fire on all re-renders except the first to check if the user still has permission to view the component
//console logging out the results to help illustrate the process
useEffect(() => {
if (didMount.current) {
axios
Expand All @@ -33,6 +37,7 @@ function Home(props: any) {
}
});

//simple button to illustrate what happens on state change which forces a re-render
let [buttonState, setButtonState] = useState(0);
const exampleChangeSomeState = () => {
setButtonState(buttonState++);
Expand Down
33 changes: 26 additions & 7 deletions server/controllers/userController.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@ import bcrypt from "bcrypt";
import jwt from "jsonwebtoken";
import dotenv from "dotenv";

//grab env variables
dotenv.config();

//import mongodb models
import User from "../models/userModel.js";
import RefreshToken from "../models/refreshTokenModel.js";

//create helper functions for handling new access / refresh tokens using the secret keys to encrypt requests
const accessToken = id => {
return jwt.sign({ userId: id }, process.env.ACCESS_TOKEN_SECRET, {
expiresIn: "30s"
Expand All @@ -17,29 +20,39 @@ const refreshToken = id => {
return jwt.sign({ userId: id }, process.env.REFRESH_TOKEN_SECRET);
};

//ensure users have an active token to view content on clientside
export const getRefreshToken = async (req, res) => {
// console.log(req.body.refreshTkn);
//grab the current token from the clientside
const refreshTkn = req.body.refreshTkn;

//if no token is passed throw error for clientside
if (!refreshTkn) {
return res.status(401).send("Token is required!");
}
//if a user has tried to manipulate the token and send a different token we run a check to verify
const decode = jwt.verify(refreshTkn, process.env.REFRESH_TOKEN_SECRET);
//if this fails we throw error to clientside
if (!decode) {
return res.status(403).send("Invalid token");
}
//however if this passes we now have access to the request data object which contains an id
const user_id = decode.id;
//now we check the mongodb db collection to see if a token exists already
const findToken = await RefreshToken.findOne({ token: refreshTkn });
//if not, we send back error message to clientside
if (!findToken) {
return res.status(403).send("Token has been expired. Sign in again.");
//otherwise, we use the helper functions to encrypt the data.
} else {
const newAccessToken = accessToken(user_id);
const newRefreshToken = refreshToken(user_id);
//then in the mongodb db collection, if the token exists we grab that data and update it with the newly
//generated tokens and set the third object param to true to replace the old values and not create a new db entry
let new_token = await RefreshToken.findOneAndUpdate(
{ token: refreshTkn },
{ token: newRefreshToken },
{ new: true }
);
//finally we send back a 200 response and the newly created tokens for clientside consumption
res.status(200).json({ newAccessToken, newRefreshToken });
}
};
Expand All @@ -59,22 +72,25 @@ export const createUser = async (req, res) => {
//create a hashed password using bcrypt
const hashedPassword = await bcrypt.hash(password, 10);
//create a document with the user data using the User Model
//using mongoose with mongodb the .create method returns a newly created object with an id.
const result = await User.create({
email,
password: hashedPassword,
name: `${firstName} ${lastName}`
});

//generate a jwt access token
//https://www.npmjs.com/package/jsonwebtoken
//again we use the helper functions to encrypt the data.
const newAccessTkn = accessToken(result._id);
const newRefreshTkn = refreshToken(result._id);

//as this is a new user, we need to create a new token for them in the token db collection
//its important we pass the token and the user, as we use the user in the model to link the two collections
const generateNewRefreshToken = await RefreshToken.create({
token: newRefreshTkn,
user: result._id
});

//finally we send the tokens to the clientside for consumption.
res.status(200).json({
accessToken: newAccessTkn,
refreshToken: newRefreshTkn,
Expand All @@ -99,25 +115,28 @@ export const loginUser = async (req, res) => {
const jwtUserID = loginUser.id;
//check that the password is correct and store in a const variable to use as truthy/falsy conditional
const passwordCorrect = await bcrypt.compare(password, hashedPassword);

//we use the helper functions to encrypt the data.
const newAccessTkn = accessToken(jwtUserID);
const newRefreshTkn = refreshToken(jwtUserID);
//we find if a token exists in the token db collection
const findToken = await RefreshToken.findOne({ user: jwtUserID });

//check if user has passed the password verification
if (passwordCorrect) {
//if they have but don't have an active token create one
if (!findToken) {
const generateNewRefreshToken = await RefreshToken.create({
token: newRefreshTkn,
user: jwtUserID
});
//otherwise, find and update the current one
} else {
let existingUserToken = await RefreshToken.findOneAndUpdate(
{ user: jwtUserID },
{ token: newRefreshTkn },
{ new: true }
);
}

//finally send back the tokens to frontend for clientside consumption
res.status(200).json({
newAccessTkn: newAccessTkn,
newRefreshTkn: newRefreshTkn
Expand Down

0 comments on commit 3de0089

Please sign in to comment.