diff --git a/client/components/app.jsx b/client/components/app.jsx index 1b4dfd0..0546d56 100644 --- a/client/components/app.jsx +++ b/client/components/app.jsx @@ -22,6 +22,7 @@ const App = props => { import('./pages/homepage')} /> import('./pages/signup')} /> + import('./pages/login')} /> () => } /> () => } /> diff --git a/client/components/pages/login.jsx b/client/components/pages/login.jsx new file mode 100644 index 0000000..08c6a46 --- /dev/null +++ b/client/components/pages/login.jsx @@ -0,0 +1,88 @@ +import React, { useState, useRef } from 'react'; +import { Redirect } from 'react-router-dom'; + +import { setToken, getToken } from '../../utilities/token-client'; + +const LogIn = props => { + //if logged in + const [tok, setTok] = useState(null); + + getToken() + .then(token => setTok(token)) + .catch(e => console.error(e)) + ; + + if (tok) { + return ; + } + + //refs + const emailRef = useRef(); + const passwordRef = useRef(); + + return ( +
+

Login

+
{ + //on submit + evt.preventDefault(); + const [result, redirect] = await handleSubmit(emailRef.current.value, passwordRef.current.value); + if (result) { + alert(result); + } + + //redirect + if (redirect) { + props.history.push('/'); + } + } + }> +
+ + +
+ +
+ + +
+ + +
+
+ ); +}; + +//DOCS: returns two values: response and OK +const handleSubmit = async (email, password) => { + email = email.trim(); //TODO: validate email on login + + //send to the auth server + const result = await fetch(`${process.env.AUTH_URI}/login`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*' + }, + body: JSON.stringify({ + email, + password, + }) + }); + + if (!result.ok) { + const err = `${result.status}: ${await result.text()}`; + console.error(err); + return [err, false]; + } + + //save the auth tokens + const authTokens = await result.json(); + + await setToken(authTokens.accessToken, authTokens.refreshToken); + + return [null, true]; +}; + +export default LogIn; \ No newline at end of file diff --git a/client/components/pages/signup.jsx b/client/components/pages/signup.jsx index d9c8b58..6621857 100644 --- a/client/components/pages/signup.jsx +++ b/client/components/pages/signup.jsx @@ -1,11 +1,24 @@ -import React, { useRef } from 'react'; +import React, { useState, useRef } from 'react'; +import { Redirect } from 'react-router-dom'; + +import { getToken } from '../../utilities/token-client'; //utilities const validateEmail = require('../../../common/utilities/validate-email.js'); const validateUsername = require('../../../common/utilities/validate-username.js'); const SignUp = props => { - //TODO: redirect if logged in + //if logged in + const [tok, setTok] = useState(null) + + getToken() + .then(token => setTok(token)) + .catch(e => console.error(e)) + ; + + if (tok) { + return ; + } //refs const emailRef = useRef(); @@ -18,18 +31,14 @@ const SignUp = props => {

Signup

{ - //on submit + async evt => { //on submit evt.preventDefault(); - const [redirect, result] = await handleSubmit(emailRef.current.value, usernameRef.current.value, passwordRef.current.value, retypeRef.current.value, contactRef.current.checked); + const [result, redirect] = await handleSubmit(emailRef.current.value, usernameRef.current.value, passwordRef.current.value, retypeRef.current.value, contactRef.current.checked); if (result) { alert(result); } - //cleanup & redirect - emailRef.current.value = usernameRef.current.value = passwordRef.current.value = retypeRef.current.value = ''; //clear input - contactRef.current.checked = false; - + //redirect if (redirect) { props.history.push('/'); } @@ -94,10 +103,10 @@ const handleSubmit = async (email, username, password, retype, contact) => { if (!result.ok) { const err = `${result.status}: ${await result.text()}`; console.error(err); - return [false, err]; + return [err, false]; } - return [true, await result.text()]; + return [await result.text(), true]; }; //returns an error message, or null on success diff --git a/client/components/panels/header.jsx b/client/components/panels/header.jsx index c63f9f3..7e8fadd 100644 --- a/client/components/panels/header.jsx +++ b/client/components/panels/header.jsx @@ -1,6 +1,8 @@ -import React from 'react'; +import React, { useState } from 'react'; import { Link } from 'react-router-dom'; +import { getToken, getRefreshToken, clearToken } from '../../utilities/token-client'; + const Visitor = () => { return (
@@ -22,19 +24,41 @@ const Member = () => { }; const logout = async () => { - //TODO: update API - await fetch('/api/accounts/logout', { method: 'POST' }) - .catch(e => console.error(e)) - ; + console.log('loging out') + const token = getToken(); + + //send to the auth server + const result = await fetch(`${process.env.AUTH_URI}/logout`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify({ + token: getRefreshToken() + }) + }); + + if (result.ok) { + await clearToken(); + } else { + console.error(await result.text()); + } }; const Header = () => { - let Options = Visitor; + const [tok, setTok] = useState(null); + + getToken() + .then(token => setTok(token)) + .catch(e => console.error(e)) + ; return (

MERN Template

- + { tok ? : }
); }; diff --git a/client/utilities/token-client.js b/client/utilities/token-client.js new file mode 100644 index 0000000..626358c --- /dev/null +++ b/client/utilities/token-client.js @@ -0,0 +1,62 @@ +import decode from 'jwt-decode'; +import Cookies from 'universal-cookie'; + +export async function setToken(access, refresh) { + const cookies = new Cookies(); + cookies.set('access', access, { path: '/' }); + cookies.set('refresh', refresh, { path: '/' }); +}; + +export async function getToken() { + const cookies = new Cookies(); + + try { + const access = cookies.get('access'); + const refresh = cookies.get('refresh'); + + if (!access || !refresh) { + return null; + } + + //if expired, refresh + if (new Date(decode(access).exp * 1000) < Date.now()) { + const result = await fetch(`${process.env.AUTH_URI}/token`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*' + }, + body: JSON.stringify({ + token: refresh + }) + }); + + const authTokens = await result.json(); + + await setToken(authTokens.accessToken, authTokens.refreshToken); + + return authTokens.accessToken; + } else { + return access; + } + } + catch (e) { + console.error(e); + return null; + } +}; + +export async function getRefreshToken() { + const cookies = new Cookies(); + + const refresh = cookies.get('refresh'); + + return refresh || null; +}; + +export async function clearToken() { + const cookies = new Cookies(); + + cookies.remove('access'); + cookies.remove('refresh'); +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 0c3db91..efbbf17 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "dotenv": "^8.2.0", "express": "^4.17.1", "html-webpack-plugin": "^5.0.0-alpha.14", + "jwt-decode": "^3.1.2", "mariadb": "^2.5.2", "raw-loader": "^4.0.2", "react": "^17.0.1", @@ -28,6 +29,7 @@ "react-router": "^5.2.0", "react-router-dom": "^5.2.0", "sequelize": "^6.4.0", + "universal-cookie": "^4.0.4", "webpack": "^5.15.0", "webpack-cli": "^4.3.1" }, @@ -1131,6 +1133,11 @@ "resolved": "https://registry.npmjs.org/@types/anymatch/-/anymatch-1.3.1.tgz", "integrity": "sha512-/+CRPXpBDpo2RK9C68N3b2cOvO0Cf5B9aPijHsoDQTHivnGSObdOF2BRQOYjojWTDy6nQvMjmqRXIxH55VjxxA==" }, + "node_modules/@types/cookie": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.3.3.tgz", + "integrity": "sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow==" + }, "node_modules/@types/eslint": { "version": "7.2.6", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.2.6.tgz", @@ -4950,6 +4957,11 @@ "node": ">=6" } }, + "node_modules/jwt-decode": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz", + "integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==" + }, "node_modules/keyv": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", @@ -8117,6 +8129,15 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/universal-cookie": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/universal-cookie/-/universal-cookie-4.0.4.tgz", + "integrity": "sha512-lbRVHoOMtItjWbM7TwDLdl8wug7izB0tq3/YVKhT/ahB4VDvWMyvnADfnJI8y6fSvsjh51Ix7lTGC6Tn4rMPhw==", + "dependencies": { + "@types/cookie": "^0.3.3", + "cookie": "^0.4.0" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -10412,6 +10433,11 @@ "resolved": "https://registry.npmjs.org/@types/anymatch/-/anymatch-1.3.1.tgz", "integrity": "sha512-/+CRPXpBDpo2RK9C68N3b2cOvO0Cf5B9aPijHsoDQTHivnGSObdOF2BRQOYjojWTDy6nQvMjmqRXIxH55VjxxA==" }, + "@types/cookie": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.3.3.tgz", + "integrity": "sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow==" + }, "@types/eslint": { "version": "7.2.6", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.2.6.tgz", @@ -13544,6 +13570,11 @@ "minimist": "^1.2.5" } }, + "jwt-decode": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz", + "integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==" + }, "keyv": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", @@ -16099,6 +16130,15 @@ "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-1.1.2.tgz", "integrity": "sha512-yvo+MMLjEwdc3RhhPYSximset7rwjMrdt9E41Smmvg25UQIenzrN83cRnF1JMzoMi9zZOQeYXHSDf7p+IQkW3Q==" }, + "universal-cookie": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/universal-cookie/-/universal-cookie-4.0.4.tgz", + "integrity": "sha512-lbRVHoOMtItjWbM7TwDLdl8wug7izB0tq3/YVKhT/ahB4VDvWMyvnADfnJI8y6fSvsjh51Ix7lTGC6Tn4rMPhw==", + "requires": { + "@types/cookie": "^0.3.3", + "cookie": "^0.4.0" + } + }, "unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", diff --git a/package.json b/package.json index fc9aaaf..66e70fa 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "dotenv": "^8.2.0", "express": "^4.17.1", "html-webpack-plugin": "^5.0.0-alpha.14", + "jwt-decode": "^3.1.2", "mariadb": "^2.5.2", "raw-loader": "^4.0.2", "react": "^17.0.1", @@ -43,6 +44,7 @@ "react-router": "^5.2.0", "react-router-dom": "^5.2.0", "sequelize": "^6.4.0", + "universal-cookie": "^4.0.4", "webpack": "^5.15.0", "webpack-cli": "^4.3.1" },