diff --git a/client/client.jsx b/client/client.jsx
index e98275d..1b52ed2 100644
--- a/client/client.jsx
+++ b/client/client.jsx
@@ -5,8 +5,11 @@ import React from 'react';
import ReactDOM from 'react-dom';
import App from './components/app';
+import TokenProvider from './components/utilities/token-provider';
ReactDOM.render(
- ,
+
+
+ ,
document.querySelector('#root')
);
diff --git a/client/components/pages/login.jsx b/client/components/pages/login.jsx
index 08c6a46..94a2ea5 100644
--- a/client/components/pages/login.jsx
+++ b/client/components/pages/login.jsx
@@ -1,18 +1,14 @@
-import React, { useState, useRef } from 'react';
+import React, { useContext, useRef } from 'react';
import { Redirect } from 'react-router-dom';
-import { setToken, getToken } from '../../utilities/token-client';
+import { TokenContext } from '../utilities/token-provider';
const LogIn = props => {
- //if logged in
- const [tok, setTok] = useState(null);
+ //context
+ const authTokens = useContext(TokenContext);
- getToken()
- .then(token => setTok(token))
- .catch(e => console.error(e))
- ;
-
- if (tok) {
+ //misplaced?
+ if (authTokens.accessToken) {
return ;
}
@@ -27,13 +23,16 @@ const LogIn = props => {
async evt => {
//on submit
evt.preventDefault();
- const [result, redirect] = await handleSubmit(emailRef.current.value, passwordRef.current.value);
- if (result) {
- alert(result);
+ const [err, newTokens] = await handleSubmit(emailRef.current.value, passwordRef.current.value);
+ if (err) {
+ alert(err);
}
- //redirect
- if (redirect) {
+ //save auth tokens and redirect
+ if (newTokens) {
+ authTokens.setAccessToken(newTokens.accessToken);
+ authTokens.setRefreshToken(newTokens.refreshToken);
+
props.history.push('/');
}
}
@@ -71,18 +70,16 @@ const handleSubmit = async (email, password) => {
})
});
+ //handle errors
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];
+ //return the new auth tokens
+ const newTokens = await result.json();
+ return [null, newTokens];
};
export default LogIn;
\ No newline at end of file
diff --git a/client/components/pages/signup.jsx b/client/components/pages/signup.jsx
index 6621857..905e389 100644
--- a/client/components/pages/signup.jsx
+++ b/client/components/pages/signup.jsx
@@ -1,22 +1,18 @@
-import React, { useState, useRef } from 'react';
+import React, { useContext, useRef } from 'react';
import { Redirect } from 'react-router-dom';
-import { getToken } from '../../utilities/token-client';
+import { TokenContext } from '../utilities/token-provider';
//utilities
const validateEmail = require('../../../common/utilities/validate-email.js');
const validateUsername = require('../../../common/utilities/validate-username.js');
const SignUp = props => {
- //if logged in
- const [tok, setTok] = useState(null)
+ //context
+ const authTokens = useContext(TokenContext);
- getToken()
- .then(token => setTok(token))
- .catch(e => console.error(e))
- ;
-
- if (tok) {
+ //misplaced?
+ if (authTokens.accessToken) {
return ;
}
diff --git a/client/components/panels/header.jsx b/client/components/panels/header.jsx
index 7e8fadd..e420a23 100644
--- a/client/components/panels/header.jsx
+++ b/client/components/panels/header.jsx
@@ -1,7 +1,7 @@
-import React, { useState } from 'react';
+import React, { useContext } from 'react';
import { Link } from 'react-router-dom';
-import { getToken, getRefreshToken, clearToken } from '../../utilities/token-client';
+import { TokenContext } from '../utilities/token-provider';
const Visitor = () => {
return (
@@ -14,51 +14,44 @@ const Visitor = () => {
};
const Member = () => {
+ const authTokens = useContext(TokenContext);
+
return (
Account
-
- Log out
+ { /* Logout button logs you out of the server too */ }
+ {
+ const result = await authTokens.tokenFetch(`${process.env.AUTH_URI}/logout`, { //NOTE: this gets overwritten as a bugfix
+ method: 'DELETE',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Access-Control-Allow-Origin': '*'
+ },
+ body: JSON.stringify({
+ token: authTokens.refreshToken
+ })
+ });
+
+ //any problems?
+ if (!result.ok) {
+ console.error(await result.text());
+ } else {
+ authTokens.setAccessToken('');
+ authTokens.setRefreshToken('');
+ }
+ }}>Log out
);
};
-const logout = async () => {
- 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 = () => {
- const [tok, setTok] = useState(null);
-
- getToken()
- .then(token => setTok(token))
- .catch(e => console.error(e))
- ;
+ const authTokens = useContext(TokenContext);
return (
MERN Template
- { tok ? : }
+ { authTokens.accessToken ? : }
);
};
diff --git a/client/components/utilities/token-provider.jsx b/client/components/utilities/token-provider.jsx
new file mode 100644
index 0000000..1788707
--- /dev/null
+++ b/client/components/utilities/token-provider.jsx
@@ -0,0 +1,88 @@
+import React, { useState, useEffect, createContext } from 'react';
+import decode from 'jwt-decode';
+
+export const TokenContext = createContext();
+
+const TokenProvider = props => {
+ const [accessToken, setAccessToken] = useState('');
+ const [refreshToken, setRefreshToken] = useState('');
+
+ //make the access and refresh tokens persist between reloads
+ useEffect(() => {
+ setAccessToken(localStorage.getItem("accessToken") || '');
+ setRefreshToken(localStorage.getItem("refreshToken") || '');
+ }, [])
+
+ React.useEffect(() => {
+ localStorage.setItem("accessToken", accessToken);
+ localStorage.setItem("refreshToken", refreshToken);
+ }, [accessToken, refreshToken]);
+
+ //wrap the default fetch function
+ const tokenFetch = async (url, options) => {
+ //use this?
+ let bearer = accessToken;
+
+ //if expired (10 minutes, normally)
+ const expired = new Date(decode(accessToken).exp * 1000) < Date.now();
+
+ if (expired) {
+ //ping the auth server for a new token
+ const response = await fetch(`${process.env.AUTH_URI}/token`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Access-Control-Allow-Origin': '*'
+ },
+ body: JSON.stringify({
+ token: refreshToken
+ })
+ });
+
+ //any errors, throw them
+ if (!response.ok) {
+ throw `${response.status}: ${await response.text()}`;
+ }
+
+ //save the new auth stuff (setting bearer as well)
+ const newAuth = await response.json();
+
+ setAccess(newAuth.accessToken);
+ setRefresh(newAuth.refreshToken);
+ bearer = newAuth.accessToken;
+
+ //BUGFIX: logging out correctly requires the new refresh token
+ if (url == `${process.env.AUTH_URI}/logout`) {
+ console.log(`logging out with refresh token: ${newAuth.refreshToken}`)
+ return fetch(`${process.env.AUTH_URI}/logout`, {
+ method: 'DELETE',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Access-Control-Allow-Origin': '*',
+ 'Authorization': `Bearer ${bearer}`
+ },
+ body: JSON.stringify({
+ token: newAuth.refreshToken
+ })
+ });
+ }
+ }
+
+ //finally, delegate to fetch
+ return fetch(url, {
+ ...options,
+ headers: {
+ ...options.headers,
+ 'Authorization': `Bearer ${bearer}`
+ }
+ });
+ };
+
+ return (
+
+ {props.children}
+
+ )
+};
+
+export default TokenProvider;
diff --git a/client/utilities/token-client.js b/client/utilities/token-client.js
deleted file mode 100644
index 626358c..0000000
--- a/client/utilities/token-client.js
+++ /dev/null
@@ -1,62 +0,0 @@
-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