diff --git a/package.json b/package.json index fc33cae..c0ba1ea 100644 --- a/package.json +++ b/package.json @@ -12,14 +12,18 @@ "prepare": "husky install" }, "dependencies": { - "firebase": "^10.7.0", "@chakra-ui/react": "^2.8.2", "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", + "axios": "^1.6.2", + "firebase": "^10.7.0", "framer-motion": "^10.16.5", "lint-staged": "^14.0.1", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-cookie": "^6.1.1", + "react-dom": "^18.2.0", + "react-router-dom": "^6.20.0", + "react-script": "^2.0.5" }, "lint-staged": { "*.{js,jsx}": "yarn run eslint" @@ -64,6 +68,8 @@ "eslint-plugin-react-refresh": "^0.4.3", "husky": "^8.0.3", "prettier": "^3.0.3", + "react-app-env": "^1.2.3", + "react-error-overlay": "6.0.9", "vite": "^4.4.5" } } diff --git a/src/App.jsx b/src/App.jsx index bc3d3fa..83a230e 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,11 +1,45 @@ import './App.css'; -import { ChakraProvider, Text } from '@chakra-ui/react' +// import from 'react'; +import { ChakraProvider } from '@chakra-ui/react' +import { Route, Routes, BrowserRouter as Router } from 'react-router-dom'; +import { CookiesProvider } from 'react-cookie'; +import Login from './components/Authentication/Login'; +import Logout from './components/Authentication/Logout'; +import Register from './components/Authentication/register'; +import Dashboard from './pages/Dashboard/Dashboard'; +import ForgotPassword from './components/Authentication/ForgotPassword'; +import EmailAction from './components/Authentication/EmailAction'; +import AUTH_ROLES from './utils/auth_config'; +import ProtectedRoute from './utils/ProtectedRoute'; + +const { ADMIN_ROLE, USER_ROLE } = AUTH_ROLES.AUTH_ROLES; const App = () => { return ( - - Hello World - + + + + + } /> + } /> + } /> + } /> + } /> + + } + /> + + + + ); }; diff --git a/src/components/Authentication/EmailAction.jsx b/src/components/Authentication/EmailAction.jsx new file mode 100644 index 0000000..0014d09 --- /dev/null +++ b/src/components/Authentication/EmailAction.jsx @@ -0,0 +1,22 @@ +// import React from 'react'; +import { useLocation, Navigate } from 'react-router-dom'; +import PropTypes from 'prop-types'; +import ResetPassword from './ResetPassword'; +import VerifyEmail from './VerifyEmail'; + +const EmailAction = ({ redirectPath }) => { + const { search } = useLocation(); + const mode = new URLSearchParams(search).get('mode'); + const code = new URLSearchParams(search).get('oobCode'); + + if (code === null) { + return ; + } + return mode === 'resetPassword' ? : ; +}; + +EmailAction.propTypes = { + redirectPath: PropTypes.string.isRequired, +}; + +export default EmailAction; diff --git a/src/components/Authentication/ForgotPassword.jsx b/src/components/Authentication/ForgotPassword.jsx new file mode 100644 index 0000000..1da3c5c --- /dev/null +++ b/src/components/Authentication/ForgotPassword.jsx @@ -0,0 +1,43 @@ +import { useState } from 'react'; +import { sendPasswordReset } from '../../utils/auth_utils'; + +const ForgotPassword = () => { + const [email, setEmail] = useState(); + const [errorMessage, setErrorMessage] = useState(); + const [confirmationMessage, setConfirmationMessage] = useState(); + + const handleForgotPassword = async e => { + try { + e.preventDefault(); + await sendPasswordReset(email); + setConfirmationMessage( + 'If the email entered is associated with an account, you should receive an email to reset your password shortly.', + ); + setErrorMessage(''); + setEmail(''); + } catch (err) { + setErrorMessage(err.message); + } + }; + + return ( +
+

Send Reset Email

+ {errorMessage &&

{errorMessage}

} +
+ setEmail(target.value)} + placeholder="Email" + /> +
+ +
+ {confirmationMessage &&

{confirmationMessage}

} + Back to Login +
+ ); +}; + +export default ForgotPassword; diff --git a/src/components/Authentication/Login.jsx b/src/components/Authentication/Login.jsx new file mode 100644 index 0000000..dbdc913 --- /dev/null +++ b/src/components/Authentication/Login.jsx @@ -0,0 +1,62 @@ +import { useState } from 'react'; +import { instanceOf } from 'prop-types'; +import { Cookies, withCookies } from '../../utils/cookie_utils'; +import { logInWithEmailAndPassword, useNavigate } from '../../utils/auth_utils'; +// import { logInWithEmailAndPassword , signInWithGoogle, useNavigate } from '../utils/auth_utils'; + +const Login = ({ cookies }) => { + const navigate = useNavigate(); + const [email, setEmail] = useState(); + const [password, setPassword] = useState(); + const [errorMessage, setErrorMessage] = useState(); + + const handleStdLogin = async e => { + try { + e.preventDefault(); + await logInWithEmailAndPassword(email, password, '/dashboard', navigate, cookies); + } catch (err) { + setErrorMessage(err.message); + } + }; + + // const handleGoogleLogin = async e => { + // try { + // e.preventDefault(); + // await signInWithGoogle('/new-user', '/dashboard', navigate, cookies); + // } catch (err) { + // setErrorMessage(err.message); + // } + // }; + + return ( +
+

Login

+ {errorMessage &&

{errorMessage}

} +
+ setEmail(target.value)} placeholder="Email" /> +
+ setPassword(target.value)} + placeholder="Password" + /> +
+ Forgot Password +
+ +
+ + {/*
+
+ +
+
*/} +
+ ); +}; + +Login.propTypes = { + cookies: instanceOf(Cookies).isRequired, +}; + +export default withCookies(Login); diff --git a/src/components/Authentication/Logout.jsx b/src/components/Authentication/Logout.jsx new file mode 100644 index 0000000..bf5cf20 --- /dev/null +++ b/src/components/Authentication/Logout.jsx @@ -0,0 +1,33 @@ +import { useState } from 'react'; +import { instanceOf } from 'prop-types'; +import { logout, useNavigate } from '../../utils/auth_utils'; +import { Cookies, withCookies } from '../../utils/cookie_utils'; + +const Logout = ({ cookies }) => { + const navigate = useNavigate(); + const [errorMessage, setErrorMessage] = useState(); + + const handleLogout = async () => { + try { + await logout('/', navigate, cookies); + } catch (err) { + setErrorMessage(err.message); + } + }; + + return ( +
+

Logout

+ {errorMessage &&

{errorMessage}

} + +
+ ); +}; + +Logout.propTypes = { + cookies: instanceOf(Cookies).isRequired, +}; + +export default withCookies(Logout); diff --git a/src/components/Authentication/ResetPassword.jsx b/src/components/Authentication/ResetPassword.jsx new file mode 100644 index 0000000..0c1ae26 --- /dev/null +++ b/src/components/Authentication/ResetPassword.jsx @@ -0,0 +1,59 @@ +import { useState } from 'react'; +import PropTypes from 'prop-types'; +import { confirmNewPassword } from '../../utils/auth_utils'; + +const ResetPassword = ({ code }) => { + const [password, setPassword] = useState(); + const [checkPassword, setCheckPassword] = useState(); + const [errorMessage, setErrorMessage] = useState(); + const [confirmationMessage, setConfirmationMessage] = useState(); + const handleResetPassword = async e => { + try { + e.preventDefault(); + if (password !== checkPassword) { + throw new Error("Passwords don't match"); + } + await confirmNewPassword(code, password); + setConfirmationMessage('Password changed. You can now sign in with your new password.'); + setErrorMessage(''); + setPassword(''); + } catch (err) { + setErrorMessage(err.message); + } + }; + return ( +
+

Reset Password

+ {errorMessage &&

{errorMessage}

} + {!confirmationMessage && ( +
+ setPassword(target.value)} + placeholder="New Password" + /> +
+ setCheckPassword(target.value)} + placeholder="Re-enter Password" + /> +
+ +
+ )} + {confirmationMessage && ( +
+

{confirmationMessage}

+ Back to Login +
+ )} +
+ ); +}; + +ResetPassword.propTypes = { + code: PropTypes.string.isRequired, +}; + +export default ResetPassword; diff --git a/src/components/Authentication/VerifyEmail.jsx b/src/components/Authentication/VerifyEmail.jsx new file mode 100644 index 0000000..7d2d3c6 --- /dev/null +++ b/src/components/Authentication/VerifyEmail.jsx @@ -0,0 +1,43 @@ +import { useState, useEffect } from 'react'; +import PropTypes from 'prop-types'; +import { confirmVerifyEmail } from '../../utils/auth_utils'; + +const VerifyEmail = ({ code }) => { + const [errorMessage, setErrorMessage] = useState(); + const [confirmationMessage, setConfirmationMessage] = useState(); + + useEffect(() => { + const verifyEmail = async () => { + try { + await confirmVerifyEmail(code); + setConfirmationMessage( + 'Your email has been verified. You can now sign in with your new account.', + ); + setErrorMessage(''); + } catch (err) { + setErrorMessage(err.message); + } + }; + + verifyEmail(); + }, [code]); + + return ( +
+

Verify Email

+ {errorMessage &&

{errorMessage}

} + {confirmationMessage && ( +
+

{confirmationMessage}

+ Back to Login +
+ )} +
+ ); +}; + +VerifyEmail.propTypes = { + code: PropTypes.string.isRequired, +}; + +export default VerifyEmail; diff --git a/src/components/Authentication/register.jsx b/src/components/Authentication/register.jsx new file mode 100644 index 0000000..a6f336e --- /dev/null +++ b/src/components/Authentication/register.jsx @@ -0,0 +1,72 @@ +import { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { registerWithEmailAndPassword } from '../../utils/auth_utils'; + +const Register = () => { + const [email, setEmail] = useState(); + const [password, setPassword] = useState(); + const [checkPassword, setCheckPassword] = useState(); + const [errorMessage, setErrorMessage] = useState(); + // const [role, setRole] = useState(); + const navigate = useNavigate(); + + const handleSubmit = async e => { + e.preventDefault(); + try { + if (password !== checkPassword) { + throw new Error("Passwords don't match"); + } + await registerWithEmailAndPassword(email, password, 'admin', navigate, '/'); + } catch (error) { + setErrorMessage(error.message); + } + }; + + // /** + // * This function handles signing up with Google + // * If the user logs in and is new, they are directed to a new-user path + // * If the user logs in and isn't new, they are directed to the dashboard. + // * If the user fails to log in, they are directed back to the login screen + // */ + // const handleGoogleSignIn = async e => { + // try { + // e.preventDefault(); + // await signInWithGoogle('/new-user', '/logout', navigate, cookies); + // } catch (err) { + // setErrorMessage(err.message); + // } + // }; + + return ( +
+

Register

+
+ setEmail(target.value)} placeholder="Email" /> +
+ {/* setRole(target.value)} placeholder="Role" /> +
*/} + setPassword(target.value)} + placeholder="Password" + type="password" + /> +
+ setCheckPassword(target.value)} + placeholder="Re-enter Password" + type="password" + /> +
+ + {/*
+ +
*/} +
+

{errorMessage}

+
+ ); +}; + +export default Register; diff --git a/src/pages/Dashboard/Dashboard.jsx b/src/pages/Dashboard/Dashboard.jsx new file mode 100644 index 0000000..9f49fb5 --- /dev/null +++ b/src/pages/Dashboard/Dashboard.jsx @@ -0,0 +1,9 @@ +const Dashboard = () => { + return ( +
+

Dashboard

+
+ ); +}; + +export default Dashboard; diff --git a/src/utils/ProtectedRoute.jsx b/src/utils/ProtectedRoute.jsx new file mode 100644 index 0000000..e77e62d --- /dev/null +++ b/src/utils/ProtectedRoute.jsx @@ -0,0 +1,59 @@ +import { useState, useEffect } from 'react'; +import { Navigate } from 'react-router-dom'; +import { PropTypes, instanceOf } from 'prop-types'; +import { withCookies, cookieKeys, Cookies, clearCookies } from './cookie_utils'; +import { refreshToken } from './auth_utils'; + +const userIsAuthenticated = async (roles, cookies) => { + try { + const accessToken = await refreshToken(cookies); + if (!accessToken) { + return false; + } + // const loggedIn = await NPOBackend.get(`/auth/verifyToken/${accessToken}`); + // return roles.includes(cookies.get(cookieKeys.ROLE)) && loggedIn.status === 200; + return roles.includes(cookies.get(cookieKeys.ROLE)); + } catch (err) { + clearCookies(cookies); + return false; + } +}; + +/** + * Protects a route from unauthenticated users + * @param {Component} children The component the user is trying to access + * @param {string} redirectPath The path to redirect the user to if they're not logged in + * @param {Array} roles A list of roles that are allowed to access the route + * @param {Cookies} cookies The user's current cookies + * @returns The relevant path to redirect the user to depending on authentication state. + */ +const ProtectedRoute = ({ Component, redirectPath, roles, cookies }) => { + const [isLoading, setIsLoading] = useState(true); + const [isAuthenticated, setIsAuthenticated] = useState(false); + + useEffect(() => { + const checkAuthentication = async () => { + const authenticated = await userIsAuthenticated(roles, cookies); + setIsAuthenticated(authenticated); + setIsLoading(false); + }; + + checkAuthentication(); + }, [roles, cookies]); + if (isLoading) { + return

LOADING...

; + } + if (isAuthenticated) { + return ; + } + return ; +}; + +ProtectedRoute.propTypes = { + Component: PropTypes.elementType.isRequired, + redirectPath: PropTypes.string.isRequired, + roles: PropTypes.arrayOf(PropTypes.string).isRequired, + cookies: instanceOf(Cookies).isRequired, +}; + +export default withCookies(ProtectedRoute); diff --git a/src/utils/auth_config.js b/src/utils/auth_config.js new file mode 100644 index 0000000..113a7d6 --- /dev/null +++ b/src/utils/auth_config.js @@ -0,0 +1,7 @@ +// Sample roles, feel free to change +const AUTH_ROLES = { + ADMIN_ROLE: 'admin', + USER_ROLE: 'General', +}; + +export default { AUTH_ROLES }; diff --git a/src/utils/auth_utils.js b/src/utils/auth_utils.js new file mode 100644 index 0000000..0c0a945 --- /dev/null +++ b/src/utils/auth_utils.js @@ -0,0 +1,349 @@ +import axios from 'axios'; +import { initializeApp } from 'firebase/app'; +import { + getAuth, + signInWithEmailAndPassword, + signOut, + GoogleAuthProvider, + signInWithPopup, + getAdditionalUserInfo, + createUserWithEmailAndPassword, + sendEmailVerification, + sendPasswordResetEmail, + confirmPasswordReset, + applyActionCode, +} from 'firebase/auth'; +import { useNavigate } from 'react-router-dom'; +import { cookieKeys, cookieConfig, clearCookies } from './cookie_utils'; + +import AUTH_ROLES from './auth_config'; + +const { USER_ROLE } = AUTH_ROLES.AUTH_ROLES; + +// Using Firebase Web version 9 +const firebaseConfig = { + apiKey: import.meta.env.VITE_FIREBASE_APIKEY, + authDomain: import.meta.env.VITE_FIREBASE_AUTHDOMAIN, + projectId: import.meta.env.VITE_FIREBASE_PROJECTID, + storageBucket: import.meta.env.VITE_FIREBASE_STORAGEBUCKET, + messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGINGSENDERID, + appId: import.meta.env.VITE_FIREBASE_APPID, +}; + +const app = initializeApp(firebaseConfig); +const auth = getAuth(app); + +// TEMP: Make sure to remove +const NPOBackend = axios.create({ + baseURL: 'http://localhost:3001', + withCredentials: true, +}); + +const refreshUrl = `https://securetoken.googleapis.com/v1/token?key=${ + import.meta.env.VITE_FIREBASE_APIKEY +}`; + +/** + * Sets a cookie in the browser + * @param {string} key key for the cookie + * @param {string} value value for the cookie + * @param {cookieConfig} config cookie config to use + */ +const setCookie = (key, value, config) => { + let cookie = `${key}=${value}; max-age=${config.maxAge}; path=${config.path}`; + if (config.domain) { + cookie += `; domain=${config.domain}`; + } + if (config.secure) { + cookie += '; secure'; + } + document.cookie = cookie; +}; + +/** + * Returns the current user synchronously + * @param {Auth} authInstance + * @returns The current user (or undefined) + */ +const getCurrentUser = authInstance => + new Promise((resolve, reject) => { + const unsubscribe = authInstance.onAuthStateChanged( + user => { + unsubscribe(); + resolve(user); + }, + err => { + reject(err); + }, + ); + }); + +// Refreshes the current user's access token by making a request to Firebase +const refreshToken = async () => { + const currentUser = await getCurrentUser(auth); + if (currentUser) { + const refreshT = currentUser.refreshToken; + const { + data: { access_token: idToken }, + } = await axios.post(refreshUrl, { + grant_type: 'refresh_token', + refresh_token: refreshT, + }); + // Sets the appropriate cookies after refreshing access token + setCookie(cookieKeys.ACCESS_TOKEN, idToken, cookieConfig); + // const user = await NPOBackend.get(`/users/${auth.currentUser.uid}`); + // setCookie(cookieKeys.ROLE, user.data.user.role, cookieConfig); + setCookie(cookieKeys.ROLE, 'admin', cookieConfig); + return idToken; + } + return null; +}; + +/** + * Makes requests to add user to NPO DB. Deletes user if Firebase error + * @param {string} email + * @param {string} userId + * @param {string} role + * @param {bool} signUpWithGoogle true if user used Google provider to sign in + * @param {string} password + */ +const createUserInDB = async (email, userId, role, signUpWithGoogle, password = null) => { + try { + if (signUpWithGoogle) { + await NPOBackend.post('/users/create', { email, userId, role, registered: false }); + } else { + await NPOBackend.post('/users/create', { email, userId, role, registered: true }); + } + } catch (err) { + // Since this route is called after user is created in firebase, if this + // route errors out, that means we have to discard the created firebase object + if (!signUpWithGoogle) { + await signInWithEmailAndPassword(auth, email, password); + } + const userToBeTerminated = await auth.currentUser; + userToBeTerminated.delete(); + throw new Error(err.message); + } +}; + +/** + * Signs a user in with Google using Firebase. Users are given USER_ROLE by default + * @param {string} newUserRedirectPath path to redirect new users to after signing in with Google Provider for the first time + * @param {string} defaultRedirectPath path to redirect users to after signing in with Google Provider + * @param {hook} navigate An instance of the useNavigate hook from react-router-dom + * @param {Cookies} cookies The user's cookies to populate + * @returns A boolean indicating whether or not the user is new + */ +const signInWithGoogle = async (newUserRedirectPath, defaultRedirectPath, navigate, cookies) => { + const provider = new GoogleAuthProvider(); + const userCredential = await signInWithPopup(auth, provider); + const newUser = getAdditionalUserInfo(userCredential).isNewUser; + cookies.set(cookieKeys.ACCESS_TOKEN, auth.currentUser.accessToken, cookieConfig); + if (newUser) { + await createUserInDB(auth.currentUser.email, userCredential.user.uid, USER_ROLE, true); + cookies.set(cookieKeys.ROLE, USER_ROLE, cookieConfig); + navigate(newUserRedirectPath); + } else { + const user = await NPOBackend.get(`/users/${auth.currentUser.uid}`); + cookies.set(cookieKeys.ROLE, user.data.user.role, cookieConfig); + if (!user.data.user.registered) { + navigate(newUserRedirectPath); + } else { + navigate(defaultRedirectPath); + } + } +}; + +/** + * When a user signs in with Google for the first time, they will need to add additional info + * This is called when the user submits the additional information which will lead to the flag + * in the backend changed so that user is not new anymore + * @param {string} redirectPath path to redirect user + * @param {hook} navigate used to redirect the user after submitted + */ +const finishGoogleLoginRegistration = async (redirectPath, navigate) => { + await NPOBackend.put(`/users/update/${auth.currentUser.uid}`); + navigate(redirectPath); +}; + +/** + * Logs a user in with email and password + * @param {string} email The email to log in with + * @param {string} password The password to log in with + * @param {string} redirectPath The path to redirect the user to after logging out + * @param {hook} navigate An instance of the useNavigate hook from react-router-dom + * @param {Cookies} cookies The user's cookies to populate + * @returns A boolean indicating whether or not the log in was successful + */ +const logInWithEmailAndPassword = async (email, password, redirectPath, navigate, cookies) => { + await signInWithEmailAndPassword(auth, email, password); + // Check if the user has verified their email. + if (!auth.currentUser.emailVerified) { + throw new Error('Please verify your email before logging in.'); + } + cookies.set(cookieKeys.ACCESS_TOKEN, auth.currentUser.accessToken, cookieConfig); + // const user = await NPOBackend.get(`/users/${auth.currentUser.uid}`); + // cookies.set(cookieKeys.ROLE, user.data.user.role, cookieConfig); + navigate(redirectPath); +}; + +/** + * Creates a user in firebase database + * @param {string} email + * @param {string} password + * @returns A UserCredential object from firebase + */ +const createUserInFirebase = async (email, password) => { + const user = await createUserWithEmailAndPassword(auth, email, password); + return user.user; +}; + +/** + * Creates a user (both in firebase and database) + * @param {string} email + * @param {string} password + * @param {string} role + * @returns A UserCredential object from firebase + */ +const createUser = async (email, password, role) => { + const user = await createUserInFirebase(email, password); + console.log(role); + // await createUserInDB(email, user.uid, role, false, password); + sendEmailVerification(user); +}; + +/** + * Registers a new user using the email provider + * @param {string} email + * @param {string} password + * @param {string} role + * @param {hook} navigate An instance of the useNavigate hook from react-router-dom + * @param {string} redirectPath path to redirect users once logged in + */ +const registerWithEmailAndPassword = async (email, password, role, navigate, redirectPath) => { + await createUser(email, password, role); + navigate(redirectPath); +}; + +/** + * Sends a password reset email given an email + * @param {string} email The email to resend password to + */ +const sendPasswordReset = async email => { + await sendPasswordResetEmail(auth, email); +}; + +/** + * Sends password reset to new account created with stated email + * @param {string} email The email to create an account with + */ +const sendInviteLink = async (email, role) => { + // generate a random password (not going to be used as new account will reset password) + const randomPassword = Math.random().toString(36).slice(-8); + const user = await createUserInFirebase(email, randomPassword); + createUserInDB(email, user.uid, role, false, randomPassword); + sendPasswordReset(email); +}; + +/** + * Completes the password reset process, given a confirmation code and new password + * @param {string} code The confirmation code sent via email to the user + * @param {string} newPassowrd The new password + */ +const confirmNewPassword = async (code, newPassword) => { + await confirmPasswordReset(auth, code, newPassword); +}; + +/** + * Applies a verification code sent to the user by email or other out-of-band mechanism. + * @param {string} code The confirmation code sent via email to the user + */ +const confirmVerifyEmail = async code => { + await applyActionCode(auth, code); +}; + +/** + * Logs a user out + * @param {string} redirectPath The path to redirect the user to after logging out + * @param {hook} navigate An instance of the useNavigate hook from react-router-dom + */ +const logout = async (redirectPath, navigate, cookies) => { + await signOut(auth); + clearCookies(cookies); + navigate(redirectPath); +}; + +/** + * Adds an axios interceptor for auth to given axiosInstance + * @param {AxiosInstance} axiosInstance instance of axios to apply interceptor to + */ +const addAuthInterceptor = axiosInstance => { + // This response interceptor will refresh the user's access token using the refreshToken helper method + axiosInstance.interceptors.response.use( + response => { + return response; + }, + async error => { + if (error.response) { + const { status, data } = error.response; + switch (status) { + case 400: + // check if 400 error was token + if (data === '@verifyToken no access token') { + // token has expired; + try { + // attempting to refresh token; + await refreshToken(); + // token refreshed, reattempting request; + const { config } = error.response; + // configure new request in a new instance; + return await axios({ + method: config.method, + url: `${config.baseURL}${config.url}`, + data: config.data, + params: config.params, + headers: config.headers, + withCredentials: true, + }); + } catch (e) { + return Promise.reject(e); + } + } else { + return Promise.reject(error); + } + default: + return Promise.reject(error); + } + } else if (error.request) { + // The request was made but no response was received + // `error.request` is an instance of XMLHttpRequest in the browser and an instance of + // http.ClientRequest in node.js + return Promise.reject(error); + } else { + // Something happened in setting up the request that triggered an Error + return Promise.reject(error); + } + }, + ); +}; + +// to be moved where NPOBackend is declared +addAuthInterceptor(NPOBackend); + +export { + NPOBackend, + auth, + useNavigate, + signInWithGoogle, + logInWithEmailAndPassword, + registerWithEmailAndPassword, + addAuthInterceptor, + sendPasswordReset, + logout, + refreshToken, + getCurrentUser, + sendInviteLink, + confirmNewPassword, + confirmVerifyEmail, + finishGoogleLoginRegistration, +}; diff --git a/src/utils/cookie_utils.js b/src/utils/cookie_utils.js new file mode 100644 index 0000000..efa4560 --- /dev/null +++ b/src/utils/cookie_utils.js @@ -0,0 +1,33 @@ +import { withCookies, Cookies } from 'react-cookie'; + +/** + * Options for the format of a cookie + * More information available here: https://www.npmjs.com/package/react-cookie + * This is used when setting a cookie + */ +const cookieConfig = { + maxAge: 3600, + path: '/', + secure: true && document.location.protocol === 'https:', +}; + +/** + * An object containing keys for cookies to store + * This is used to set and remove cookies + */ +const cookieKeys = { + ACCESS_TOKEN: 'accessToken', + ROLE: 'role', +}; + +/** + * Clears all cookies stored during log in + * @param {Cookies} cookies THe user's current cookies + */ +const clearCookies = cookies => { + Object.values(cookieKeys).forEach(value => { + cookies.remove(value); + }); +}; + +export { withCookies, Cookies, cookieConfig, cookieKeys, clearCookies };