From bffa2d6aea90966b0d02a7ae4aa41b0d1db885b9 Mon Sep 17 00:00:00 2001 From: MandelaK Date: Mon, 8 Apr 2019 05:22:36 +0300 Subject: [PATCH] feat(login) users can log in - create login form - users can log in - errors are displayed above the specific input fields and input fields have a red border if they are not validated - set up validation of input fields - configure axios - test loginReducer - test login validator function - tokens are stored in localStorage on successful login - add function for validating authentication tokens - add function for checking whether a user is logged in with valid jwt token - users are redirected on successful login to the homepage - logged in users cannot see the login form - test login form [starts #164046210] --- .gitignore | 1 + package.json | 15 +- src/axiosConfig.js | 11 ++ src/components/Header.jsx | 51 ++++-- src/components/Login.jsx | 3 +- src/containers/LoginForm.jsx | 137 +++++++++++++++ src/css/App.css | 34 ++++ src/index.jsx | 2 + src/setupTests.js | 2 + src/store/actions/actionTypes.js | 2 + src/store/actions/authActions/LoginAction.js | 29 ++++ src/store/reducers/LoginReducer.js | 28 ++++ src/store/reducers/index.js | 2 + src/tests/components/Header.test.js | 36 +++- src/tests/components/Login.test.js | 10 ++ src/tests/containers/LoginForm.test.js | 157 ++++++++++++++++++ .../__snapshots__/LoginForm.test.js.snap | 7 + .../Actions/authActions/loginAction.test.js | 71 ++++++++ src/tests/store/reducers/LoginReducer.test.js | 46 +++++ src/tests/utils/tokenValidator.test.js | 26 +++ src/tests/utils/validation.test.js | 25 +++ src/utils/tokenValidator.js | 16 ++ src/utils/validation.js | 11 ++ 23 files changed, 696 insertions(+), 26 deletions(-) create mode 100644 src/axiosConfig.js create mode 100644 src/containers/LoginForm.jsx create mode 100644 src/store/actions/authActions/LoginAction.js create mode 100644 src/store/reducers/LoginReducer.js create mode 100644 src/tests/components/Login.test.js create mode 100644 src/tests/containers/LoginForm.test.js create mode 100644 src/tests/containers/__snapshots__/LoginForm.test.js.snap create mode 100644 src/tests/store/Actions/authActions/loginAction.test.js create mode 100644 src/tests/store/reducers/LoginReducer.test.js create mode 100644 src/tests/utils/tokenValidator.test.js create mode 100644 src/tests/utils/validation.test.js create mode 100644 src/utils/tokenValidator.js create mode 100644 src/utils/validation.js diff --git a/.gitignore b/.gitignore index fb3af2f..3ecab71 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ # misc .DS_Store +.env .env.local .env.development.local .env.test.local diff --git a/package.json b/package.json index 0ff0092..84c349c 100644 --- a/package.json +++ b/package.json @@ -3,8 +3,13 @@ "version": "0.1.0", "private": true, "dependencies": { + "axios": "^0.18.0", "bootstrap": "^4.3.1", + "dotenv": "^7.0.0", + "enzyme": "^3.9.0", + "enzyme-adapter-react-16": "^1.12.1", "i": "^0.3.6", + "jsonwebtoken": "^8.5.1", "node-sass": "^4.11.0", "prop-types": "^15.7.2", "react": "^16.8.6", @@ -14,7 +19,9 @@ "react-router-dom": "^5.0.0", "react-scripts": "2.1.8", "redux": "^4.0.1", - "redux-thunk": "^2.3.0" + "redux-mock-store": "^1.5.3", + "redux-thunk": "^2.3.0", + "sinon": "^7.3.1" }, "scripts": { "start": "react-scripts start", @@ -36,13 +43,13 @@ "devDependencies": { "coverage": "^0.1.0", "coveralls": "^3.0.3", - "enzyme": "^3.9.0", - "enzyme-adapter-react-16": "^1.11.2", "enzyme-to-json": "^3.3.5", "eslint-config-airbnb": "^17.1.0", "eslint-plugin-import": "^2.16.0", "eslint-plugin-jsx-a11y": "^6.2.1", - "eslint-plugin-react": "^7.12.4" + "eslint-plugin-react": "^7.12.4", + "jest-localstorage-mock": "^2.4.0", + "moxios": "^0.4.0" }, "jest": { "snapshotSerializers": [ diff --git a/src/axiosConfig.js b/src/axiosConfig.js new file mode 100644 index 0000000..fde5a80 --- /dev/null +++ b/src/axiosConfig.js @@ -0,0 +1,11 @@ +import axios from 'axios'; + +const { REACT_APP_BASE_URL } = process.env; + +const axiosConfig = axios.create({ + baseURL: REACT_APP_BASE_URL, + headers: { + 'content-type': 'application/json', + }, +}); +export default axiosConfig; diff --git a/src/components/Header.jsx b/src/components/Header.jsx index f3dd0d6..fe49c33 100644 --- a/src/components/Header.jsx +++ b/src/components/Header.jsx @@ -5,10 +5,23 @@ import { NavLink } from 'react-router-dom'; import AuthenticationModal from './AuthenticationModal'; import store from '../store/store'; import { LOGIN, REGISTER } from '../store/actions/actionTypes'; - +import { isLoggedIn } from '../utils/tokenValidator'; class Header extends React.Component { + + state = { + LoggedIn: isLoggedIn + } + + dispatchLogin = () => { + store.dispatch({ type: LOGIN }) + } + + dispatchRegister = () => { + store.dispatch({ type: REGISTER }) + } render() { + const { LoggedIn } = this.state; return ( @@ -16,20 +29,28 @@ class Header extends React.Component { - + {LoggedIn ? ( + + ) : ( + + )} diff --git a/src/components/Login.jsx b/src/components/Login.jsx index bc1033e..a0c83bd 100644 --- a/src/components/Login.jsx +++ b/src/components/Login.jsx @@ -1,4 +1,5 @@ import React from 'react'; +import LoginForm from '../containers/LoginForm'; -const Login = () =>
Login
; +const Login = () =>
; export default Login; diff --git a/src/containers/LoginForm.jsx b/src/containers/LoginForm.jsx new file mode 100644 index 0000000..e888abd --- /dev/null +++ b/src/containers/LoginForm.jsx @@ -0,0 +1,137 @@ +import React from "react"; +import { Form, Button, Alert } from "react-bootstrap"; +import PropTypes from "prop-types"; +import loginAction from "../store/actions/authActions/LoginAction"; +import { connect } from "react-redux"; +import validateLoginForm from "../utils/validation"; +import store from "../store/store"; +import { REGISTER } from '../store/actions/actionTypes'; + +export class LoginForm extends React.Component { + static propTypes = { + email: PropTypes.string, + password: PropTypes.any, + login_user: PropTypes.object.isRequired, + loginAction: PropTypes.func.isRequired + }; + + constructor(props) { + super(props); + this.state = { + email: "", + password: "", + formErrors: {}, + touched: false, + show: true + }; + this.onAlertClose = this.onAlertClose.bind(this); + } + + handleChange = event => { + this.setState({ [event.target.name]: event.target.value, touched: true }); + }; + + + onAlertClose() { + this.setState({ + show: false + }); + } + + handleSubmit = event => { + const { email, password } = this.state; + event.preventDefault(); + const formErrors = validateLoginForm(email, password); + if (formErrors.length === 0) { + this.setState({ + formErrors: [] + }); + const data = { email, password }; + this.props.loginAction(data); + } else { + this.setState({ + formErrors: formErrors, + touched: false + }); + } + }; + + swapModal = () => { + store.dispatch({ type: REGISTER }) + } + + render() { + const { login_user } = this.props; + const { errors } = login_user; + const { formErrors, touched } = this.state; + + return ( +
+
+ + {errors && ( + + + {errors} + + + )} + Email address +
+ {(formErrors[0] === "email" && !touched) && formErrors[1]} +
+

{errors.email}

+ +
+ + Password +
+ {(formErrors[0] === "password" && !touched) && formErrors[1]} +
+

{errors.password}

+ +
+ +

Don't have an account? Click here to register.

+
+
+ ); + } +} + +export const mapStateToProps = state => { + const { login_user } = state; + return { + login_user + }; +}; + +export default connect( + mapStateToProps, + { loginAction } +)(LoginForm); diff --git a/src/css/App.css b/src/css/App.css index e5158cf..701f0f7 100644 --- a/src/css/App.css +++ b/src/css/App.css @@ -71,6 +71,40 @@ body { font-weight: 500; } +.swap-modal-span { + cursor: pointer; + text-decoration: chocolate; + color: chocolate; +} + +.swap-modal-span:hover { + opacity: 0.8; + text-decoration: underline; +} + +.swap-modal-text { + font-size: 14px; +} + .modal-dialog { height: 100%; } +.red-form-border { + border: 1px solid red; +} + +.password-input { + margin-top: 0; +} + +.btn-one{ + background: #792525; + border: none; + color: #fcc810; +} +.btn-one:hover{ + opacity: 0.7; + background: #792525; + border: none; + color: #fcc810; +} diff --git a/src/index.jsx b/src/index.jsx index 291c67b..ce7fcb2 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -8,6 +8,8 @@ import Routes from './routes'; import * as serviceWorker from './serviceWorker'; import store from './store/store'; +require('dotenv').config(); + ReactDOM.render( diff --git a/src/setupTests.js b/src/setupTests.js index 82edfc9..1845cff 100644 --- a/src/setupTests.js +++ b/src/setupTests.js @@ -1,4 +1,6 @@ import { configure } from 'enzyme'; import Adapter from 'enzyme-adapter-react-16'; +require('jest-localstorage-mock'); + configure({ adapter: new Adapter() }); diff --git a/src/store/actions/actionTypes.js b/src/store/actions/actionTypes.js index e7ed888..9d94d4b 100644 --- a/src/store/actions/actionTypes.js +++ b/src/store/actions/actionTypes.js @@ -1,3 +1,5 @@ export const SHOW_MODAL = 'SHOW_MODAL'; export const REGISTER = 'REGISTER'; export const LOGIN = 'LOGIN'; +export const LOGIN_SUCCESS = 'LOGIN_SUCCESS'; +export const LOGIN_FAIL = 'LOGIN_FAIL'; diff --git a/src/store/actions/authActions/LoginAction.js b/src/store/actions/authActions/LoginAction.js new file mode 100644 index 0000000..6bcc25b --- /dev/null +++ b/src/store/actions/authActions/LoginAction.js @@ -0,0 +1,29 @@ +import axiosConfig from '../../../axiosConfig'; +import { LOGIN_SUCCESS, LOGIN_FAIL } from '../actionTypes'; +import ShowModal from '../changeFormAction'; + +const loginAction = loginData => dispatch => axiosConfig.request( + { + method: 'post', + url: '/user/login/', + data: { user: loginData }, + }, +) + .then((response) => { + const { token } = response.data.user; + window.localStorage.setItem('token', token); + dispatch(ShowModal({ + modalShow: false, + })); + dispatch({ + type: LOGIN_SUCCESS, + }); + }) + .catch((error) => { + dispatch({ + type: LOGIN_FAIL, + payload: error.response.data.errors.error, + }); + }); + +export default loginAction; diff --git a/src/store/reducers/LoginReducer.js b/src/store/reducers/LoginReducer.js new file mode 100644 index 0000000..367502f --- /dev/null +++ b/src/store/reducers/LoginReducer.js @@ -0,0 +1,28 @@ +import { LOGIN_SUCCESS, LOGIN_FAIL } from '../actions/actionTypes'; +import { isLoggedIn } from '../../utils/tokenValidator'; + +const initialState = { + logged_in: !!isLoggedIn, + errors: '', +}; + +const loginReducer = (state = initialState, action) => { + switch (action.type) { + case LOGIN_SUCCESS: + return { + ...state, + logged_in: true, + errors: '', + }; + case LOGIN_FAIL: + return { + ...state, + logged_in: false, + errors: action.payload, + }; + default: + return state; + } +}; + +export default loginReducer; diff --git a/src/store/reducers/index.js b/src/store/reducers/index.js index 56feabb..22d32af 100644 --- a/src/store/reducers/index.js +++ b/src/store/reducers/index.js @@ -1,8 +1,10 @@ import { combineReducers } from 'redux'; import modalReducer from './modal-reducer'; +import loginReducer from './LoginReducer'; const rootReducer = combineReducers({ modalState: modalReducer, + login_user: loginReducer, }); export default rootReducer; diff --git a/src/tests/components/Header.test.js b/src/tests/components/Header.test.js index 60e9d00..6bd0df7 100644 --- a/src/tests/components/Header.test.js +++ b/src/tests/components/Header.test.js @@ -1,10 +1,34 @@ -import { shallow, mount } from 'enzyme'; -import Header from '../../components/Header'; -import React from 'react'; +import { shallow, mount } from "enzyme"; +import Header from "../../components/Header"; +import React from "react"; +import { isLoggedIn } from '././../../utils/tokenValidator'; -describe('Header', () => { - it('renders one Header', () => { - const component = shallow(
); +describe("Header", () => { + const component = shallow(
); + const componentInstance = component.instance(); + const createSpy = toSpy => jest.spyOn(componentInstance, toSpy); + it("renders one Header", () => { expect(component).toHaveLength(1); }); + it("calls the login modal when the login link is clicked", () => { + const dispatchLogin = createSpy("dispatchLogin"); + componentInstance.forceUpdate(); + const login = component.find(".login"); + login.simulate("click"); + expect(dispatchLogin).toHaveBeenCalled(); + }); + it("calls the register modal when the register link is clicked", () => { + const dispatchRegister = createSpy("dispatchRegister"); + componentInstance.forceUpdate(); + const register = component.find(".get-started"); + register.simulate("click"); + expect(dispatchRegister).toHaveBeenCalled(); + }); + it("shows profile icon dropdown when a user logs in", () => { + + component.setState({ LoggedIn: true}) + const profileDropdown = component.find(".profile-dropdown"); + expect(profileDropdown.length).toEqual(1); + expect(component.find(".login").length).toEqual(0); + }); }); diff --git a/src/tests/components/Login.test.js b/src/tests/components/Login.test.js new file mode 100644 index 0000000..6d51c96 --- /dev/null +++ b/src/tests/components/Login.test.js @@ -0,0 +1,10 @@ +import React from "react"; +import { shallow } from "enzyme"; +import Login from "../../components/Login"; + +describe("Login", () => { + it("Should render correctly", () => { + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot; + }); +}); diff --git a/src/tests/containers/LoginForm.test.js b/src/tests/containers/LoginForm.test.js new file mode 100644 index 0000000..4d32f11 --- /dev/null +++ b/src/tests/containers/LoginForm.test.js @@ -0,0 +1,157 @@ +import React from "react"; +import { shallow } from "enzyme"; +import { LoginForm } from "../../containers/LoginForm"; +import { mapStateToProps } from "../../containers/LoginForm"; +import { Alert } from "react-bootstrap"; + +describe("", () => { + const props = { + errors: "", + loginAction: jest.fn(() => { + Promise.resolve(); + }), + login_user: { errors: "" }, + validateLoginForm: jest.fn() + }; + const wrapper = shallow(); + const wrapperInstance = wrapper.instance(); + it("should properly show registration form when the span is clicked", () => { + const createSpy = toSpy => jest.spyOn(wrapperInstance, toSpy); + const swapModal = createSpy("swapModal"); + wrapperInstance.forceUpdate(); + const span = wrapper.find("span"); + span.simulate("click"); + expect(swapModal).toHaveBeenCalled(); + }); + it("Should update state whenever data is entered in the input fields", () => { + const event = { + target: { + name: "email", + value: "user@me.com" + } + }; + wrapperInstance.handleChange(event); + expect(wrapperInstance.state.email).toEqual(event.target.value); + }); + it("Should dismiss the alert when the close button is clicked", () => { + const event = { + target: { + onAlertClose: jest.fn() + } + }; + wrapperInstance.onAlertClose(event); + expect(wrapperInstance.state.show).toEqual(false); + }); + it("Should dispatch the loginAction function when the form is submitted with correct input", () => { + const event = { + preventDefault: jest.fn() + }; + const state = { + email: "mimi@mail.com", + password: "password" + }; + wrapperInstance.setState(state); + wrapperInstance.handleSubmit(event); + expect(props.loginAction).toHaveBeenCalledWith(state); + }); + it("Should return state as being an empty state when there were no errors from the form validation", () => { + const event = { + preventDefault: jest.fn(), + target: { + validateLoginForm: jest.fn() + } + }; + const state = { + email: "mimi@mail.com", + password: "password", + formErrors: {} + }; + wrapperInstance.setState(state); + wrapperInstance.handleSubmit(event); + expect(wrapperInstance.state.formErrors).toEqual([]); + }); + it("Should set errors in the state if the login form is empty on submit", () => { + const event = { + preventDefault: jest.fn(), + target: { + validateLoginForm: jest.fn() + } + }; + const state = { + email: "", + password: "", + formErrors: {} + }; + wrapperInstance.setState(state); + wrapperInstance.handleSubmit(event); + expect(wrapperInstance.state.formErrors).toEqual([ + "email", + "Please provide an email address!" + ]); + }); + it("Should return null if we pass in a form with errors", () => { + const event = { + preventDefault: jest.fn(), + target: { + handleSubmit: jest.fn() + } + }; + const state = { + email: "", + password: "", + formErrors: {} + }; + wrapperInstance.setState(state); + const handleSubmit = jest.fn(); + wrapperInstance.handleSubmit(event); + expect(handleSubmit).not.toHaveReturned(); + }); +}); + +describe(" when there are errors from a request", () => { + const props = { + errors: "", + loginAction: jest.fn(() => { + Promise.resolve(); + }), + login_user: { errors: "A user with this email and password was not found" }, + validateLoginForm: jest.fn() + }; + const wrapper = shallow(); + expect(wrapper.find(Alert)).toHaveLength(1); +}); + +describe(" when there are errors in the password input", () => { + const props = { + errors: "", + loginAction: jest.fn(() => { + Promise.resolve(); + }), + login_user: { errors: "" }, + validateLoginForm: jest.fn() + }; + const state = { + email: "test@mail.com", + password: "", + formErrors: ["password", "Please provide your password"] + }; + const wrapper = shallow(); + const wrapperInstance = wrapper.instance(); + wrapperInstance.setState(state); + expect(wrapper.find("div.password-warning").text()).toEqual( + "Please provide your password" + ); +}); + +describe("mapStateToProps function", () => { + it("should map the state we pass to the props", () => { + const state = { + login_user: { + logged_in: false, + errors: "" + } + }; + const props = mapStateToProps(state); + expect(props).toEqual(state); + }); +}); diff --git a/src/tests/containers/__snapshots__/LoginForm.test.js.snap b/src/tests/containers/__snapshots__/LoginForm.test.js.snap new file mode 100644 index 0000000..d2bb7fa --- /dev/null +++ b/src/tests/containers/__snapshots__/LoginForm.test.js.snap @@ -0,0 +1,7 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` Should display the password input field 1`] = ` + + + +`; diff --git a/src/tests/store/Actions/authActions/loginAction.test.js b/src/tests/store/Actions/authActions/loginAction.test.js new file mode 100644 index 0000000..4ad4a67 --- /dev/null +++ b/src/tests/store/Actions/authActions/loginAction.test.js @@ -0,0 +1,71 @@ +import axiosconfig from "../../../../axiosConfig"; +import moxios from "moxios"; +import configureMockStore from "redux-mock-store"; +import thunk from "redux-thunk"; +import loginAction from "../../../../store/actions/authActions/LoginAction"; + +const middleware = [thunk]; +const mockStore = configureMockStore(middleware); +const store = mockStore(); + +const mockResponseSuccess = { + status: 200, + data: { + user: { token: "asdafsdfweqrwfadfwdfoweirhqwoerhpqwoef" } + } +}; +const mockResponseFailure = { + status: 400, + response: { + data: { + errors: { + error: "A user with this email address and password was not found" + } + } + } +}; +describe("async actions", () => { + + beforeEach(() => { + moxios.install(axiosconfig); + store.clearActions(); + + }); + afterEach(() => { + moxios.uninstall(axiosconfig); + }); + it("it dispatches SHOW_MODAL and LOGIN_SUCCESS when the user succesfully signs in", () => { + moxios.wait(() => { + const request = moxios.requests.mostRecent(); + request.respondWith({ + status: 200, + response: mockResponseSuccess.data + }); + }); + const expectedActions = ["SHOW_MODAL", "LOGIN_SUCCESS"]; + + return store.dispatch(loginAction()).then(() => { + const dispatchedActions = store.getActions(); + const actionTypes = dispatchedActions.map(action => action.type); + + expect(actionTypes).toEqual(expectedActions); + }); + }); + it("dispatches LOGIN_FAIL when the login request fails", () => { + moxios.wait(() => { + const request = moxios.requests.mostRecent(); + request.respondWith({ + status: 400, + response: mockResponseFailure.response.data + }); + }); + const expectedActions = ["LOGIN_FAIL"]; + + return store.dispatch(loginAction()).then(() => { + const dispatchedActions = store.getActions(); + const actionTypes = dispatchedActions.map(action => action.type); + + expect(actionTypes).toEqual(expectedActions); + }); + }); +}); diff --git a/src/tests/store/reducers/LoginReducer.test.js b/src/tests/store/reducers/LoginReducer.test.js new file mode 100644 index 0000000..fb6a373 --- /dev/null +++ b/src/tests/store/reducers/LoginReducer.test.js @@ -0,0 +1,46 @@ +import { LOGIN_SUCCESS, LOGIN_FAIL } from "../../../store/actions/actionTypes"; +import loginReducer from "../../../store/reducers/LoginReducer"; + +describe("loginReducer", () => { + const initialState = { + logged_in: false, + errors: "" + }; + it("should dispatch success action on login success", () => { + const loginSuccess = { + type: LOGIN_SUCCESS + }; + const successState = { + logged_in: true, + errors: "" + }; + expect(loginReducer(initialState, loginSuccess)).toEqual(successState); + }); + it("should dispatch failure action on login fail", () => { + const loginFail = { + type: LOGIN_FAIL, + payload: "A user with this email and password was not found" + }; + const failureState = { + logged_in: false, + errors: loginFail.payload + }; + const successState = { + logged_in: true, + errors: "" + }; + expect(loginReducer(initialState, loginFail)).toEqual(failureState); + expect(loginReducer(initialState, loginFail)).not.toEqual(successState); + }); + it("should return current state if action recieved doesn't match success or fail", () => { + const LOGIN_TRIAL = "LOGIN_TRIAL"; + const invalidType = { + type: LOGIN_TRIAL + }; + const defaultState = { + logged_in: false, + errors: "" + }; + expect(loginReducer(initialState, invalidType)).toEqual(defaultState); + }); +}); diff --git a/src/tests/utils/tokenValidator.test.js b/src/tests/utils/tokenValidator.test.js new file mode 100644 index 0000000..d0deb60 --- /dev/null +++ b/src/tests/utils/tokenValidator.test.js @@ -0,0 +1,26 @@ +import * as jwt from "jsonwebtoken"; + +import { Authenticate } from "../../utils/tokenValidator"; + +describe("Authenticate function", () => { + const SECRET_KEY = process.env.REACT_APP_SECRET_KEY; + it("Should not authenticate an invalid token", () => { + const data = Authenticate("asdfsdfsidfasd"); + expect(data).toEqual(false); + }); + it("Should authenticate a valid token", () => { + const dummy_object = { example: "object" }; + const token = jwt.sign(dummy_object, SECRET_KEY, { expiresIn: 60 * 60 }); + const data = Authenticate(token, SECRET_KEY); + expect(data).toHaveProperty("example", "object"); + }); + it("Should not authenticate an expired token", () => { + const expiredObject = { + expired: "object", + iat: Math.floor(Date.now() / 1000) - 30 + }; + const expiredToken = jwt.sign(expiredObject, SECRET_KEY); + const data = Authenticate(expiredToken); + expect(data).toEqual(false); + }); +}); diff --git a/src/tests/utils/validation.test.js b/src/tests/utils/validation.test.js new file mode 100644 index 0000000..4d8c1f4 --- /dev/null +++ b/src/tests/utils/validation.test.js @@ -0,0 +1,25 @@ +import validateLoginForm from "../../utils/validation"; + +describe("Blank email validation", () => { + it("validates whether the user actually added an email address", () => { + expect(validateLoginForm("", "")).toEqual([ + "email", + "Please provide an email address!" + ]); + }); +}); + +describe("Blank password validation", () => { + it("validates whether the user actually added a password", () => { + expect(validateLoginForm("test@email.com", "")).toEqual([ + "password", + "Please provide your password" + ]); + }); +}); + +describe("Properly filled login form", () => { + it("shoud return an empty array, indicating no errors in validation", () => { + expect(validateLoginForm("test@email.com", "password")).toEqual([]); + }); +}); diff --git a/src/utils/tokenValidator.js b/src/utils/tokenValidator.js new file mode 100644 index 0000000..57cd359 --- /dev/null +++ b/src/utils/tokenValidator.js @@ -0,0 +1,16 @@ +import * as jwt from 'jsonwebtoken'; + +// by authenticating the token, we get back the user details +export const Authenticate = (token) => { + try { + const details = jwt.verify(token, process.env.REACT_APP_SECRET_KEY); + if (details.exp > Date.now() / 1000) { + return details; + } return false; + } catch (error) { + return false; + } +}; + +const token = localStorage.getItem('token'); +export const isLoggedIn = Authenticate(token); diff --git a/src/utils/validation.js b/src/utils/validation.js new file mode 100644 index 0000000..5a7023e --- /dev/null +++ b/src/utils/validation.js @@ -0,0 +1,11 @@ +function validateLoginForm(email, password) { + const errors = []; + if (email.length === 0) { + errors.push('email', 'Please provide an email address!'); + } else if (password.length === 0) { + errors.push('password', 'Please provide your password'); + } + return errors; +} + +export default validateLoginForm;