From e183e60fe23d01e7948b40e591e2a2907eb8092f Mon Sep 17 00:00:00 2001 From: wil Date: Thu, 4 Apr 2024 16:10:14 -0400 Subject: [PATCH] protected routes, 404 page, auth with localstorage, login redirects --- .../Authentication/Authentication.tsx | 115 +++++++++++------- app/contexts/Authentication/types.tsx | 6 +- app/modules/withProtectedRoute/index.tsx | 26 ++++ app/routes/$.tsx | 27 ++++ app/routes/IdealEnigma.tsx | 8 +- app/routes/Login.tsx | 20 +-- 6 files changed, 141 insertions(+), 61 deletions(-) create mode 100644 app/modules/withProtectedRoute/index.tsx create mode 100644 app/routes/$.tsx diff --git a/app/contexts/Authentication/Authentication.tsx b/app/contexts/Authentication/Authentication.tsx index 27d0d7594f..8e770955d1 100644 --- a/app/contexts/Authentication/Authentication.tsx +++ b/app/contexts/Authentication/Authentication.tsx @@ -1,7 +1,7 @@ import { createContext, - useEffect, useCallback, + useEffect, useState, ReactNode, Dispatch, @@ -12,52 +12,43 @@ import { useCookies } from 'react-cookie'; import { SessionResponse, AuthState, + STORAGE_KEYS } from "./types"; import { CreateSession, DestroySession, GetSession } from "./utilities"; export const AuthenticationContext = createContext<{ - authState?: AuthState, - setAuthState?: Dispatch>, - persistLoginOrOut?: ()=>void, + authState: AuthState, + setAuthState?: Dispatch>, loginAndGetUserInfo?: ( usernameOrEmail: string, password:string, failureCallback?:()=>void )=>void; logout?: ()=>void; -}>({}); +}>({ + authState:{} +}); export const AuthenticationProvider = ({children}:{children:ReactNode})=>{ - const [authCookies, setAuthCookie, removeAuthCookie] = useCookies(['userInfo']); - const [authState, setAuthState] = useState(undefined); + console.log('auth provider rerender'); + const [authCookies, setAuthCookie, removeAuthCookie] = useCookies([STORAGE_KEYS.USER_INFO]); + const [authState, setAuthState] = useState({}); - const persistLoginOrOut = useCallback(()=>{ - GetSession().then(( - res: SessionResponse - )=>{ - if(res.success && res.user){ - setAuthCookie('userInfo', res.user); - setAuthState({ - isLoggedIn: true, - userInfo: res.user - }); - }else{ - removeAuthCookie('userInfo'); - } - }); - }, []); - - const loginAndGetUserInfo = (usernameOrEmail: string, password:string, failureCallback?:()=>void)=>{ + const loginAndGetUserInfo = useCallback(( + usernameOrEmail: string, + password:string, + failureCallback?:()=>void + )=>{ CreateSession({usernameOrEmail, password}) .then((respond)=>{ GetSession().then(( res: SessionResponse )=>{ if(res.success && res.user && setAuthState){ - setAuthCookie('userInfo', res.user); setAuthState({ isLoggedIn: true, + sessionData: JSON.stringify(res.user), userInfo: res.user }); }else{ @@ -65,29 +56,69 @@ export const AuthenticationProvider = ({children}:{children:ReactNode})=>{ } }); }); - } - - const logout = ()=>{ - DestroySession().then((res)=>{ - if (res && setAuthState) { - setAuthState({ - isLoggedIn: false, - userInfo: undefined - }); - } - }); - } - useEffect(()=>{ - if (authCookies !== undefined && authState===undefined){ - persistLoginOrOut(); - } },[]); + const setLogoutState = useCallback(()=>{ + setAuthState({ + isLoggedIn: false, + sessionData: null, + userInfo: undefined + }); + },[]); + + const logout = useCallback(()=>{ + DestroySession().then((res)=>{ + if (res) setLogoutState(); + }); + },[]); + + //Set Session upon Auth State Changes + useEffect(()=>{ + //On First Load + if(authState.isLoggedIn === undefined){ + //Persist Login if Session Data exist + const sessionData = localStorage.getItem(STORAGE_KEYS.USER_INFO); + console.log(`Session Data: ${sessionData?.substring(0, 30)}${sessionData ? '...':''}`); + if (sessionData !== null){ + console.log('Getting session to persist login'); + GetSession().then(( + res: SessionResponse + )=>{ + console.log('Session Returned'); + console.log(res); + if(res.success && res.user){ + console.log('Setting Auth State'); + setAuthState({ + isLoggedIn: true, + sessionData: JSON.stringify(res.user), + userInfo: res.user + }); + }else{ + console.log('Session expired, setting State'); + setLogoutState(); + } + }); + }else{ + setLogoutState(); + } + } + + // Propagate session data to localstorage + if(authState.sessionData !== null && authState.sessionData !== undefined){ + localStorage.setItem(STORAGE_KEYS.USER_INFO, authState.sessionData); + setAuthCookie(STORAGE_KEYS.USER_INFO, authState.userInfo); + } + // Propagate Logout + else if (authState.sessionData === null){ + localStorage.removeItem(STORAGE_KEYS.USER_INFO); + removeAuthCookie(STORAGE_KEYS.USER_INFO); + } + },[authState]); + return( diff --git a/app/contexts/Authentication/types.tsx b/app/contexts/Authentication/types.tsx index 2daea795e5..b1e05ae494 100644 --- a/app/contexts/Authentication/types.tsx +++ b/app/contexts/Authentication/types.tsx @@ -1,3 +1,6 @@ +export enum STORAGE_KEYS{ + USER_INFO = "user_info", +} export enum AUTH_STATUS { LOGGED_IN = "logged_in", LOGGING = "logging", @@ -5,7 +8,8 @@ export enum AUTH_STATUS { } export type AuthState = { - isLoggedIn: boolean; + isLoggedIn?: boolean; + sessionData?: string | null; userInfo?: UserInfo; } export interface UserInfo { diff --git a/app/modules/withProtectedRoute/index.tsx b/app/modules/withProtectedRoute/index.tsx new file mode 100644 index 0000000000..823f77a476 --- /dev/null +++ b/app/modules/withProtectedRoute/index.tsx @@ -0,0 +1,26 @@ +import { useContext, useEffect, ComponentType } from "react"; +import { useNavigate, useLocation } from "@remix-run/react"; +import { AuthenticationContext } from "~/contexts/Authentication"; + +export const withProtectionRoute =

(Component: ComponentType

) => (rest:P) => { + const { authState } = useContext(AuthenticationContext); + console.log(authState); + if (authState.isLoggedIn === undefined ) { + return <>waiting; + } + if (authState.isLoggedIn === false) { + return + } + return ( + + ); +} + +const RedirectToLogin = ()=>{ + const navigate = useNavigate(); + const {pathname} = useLocation(); + useEffect(()=>{ + navigate(`/login?redirect=${pathname}`); + },[]); + return <>redirecting +} \ No newline at end of file diff --git a/app/routes/$.tsx b/app/routes/$.tsx new file mode 100644 index 0000000000..78d0f29049 --- /dev/null +++ b/app/routes/$.tsx @@ -0,0 +1,27 @@ +import { json } from "@remix-run/router"; +import { withProtectionRoute } from "~/modules/withProtectedRoute"; + +import { faChevronLeft } from "@fortawesome/pro-solid-svg-icons"; +import { P, H1, ButtonLink } from "~/components"; +export const loader = () => { + return json(null, { status: 404 }); +}; + +const Page404 = withProtectionRoute(()=>{ + return( +

+
+

Oops, seems like this is not a page that exists!

+

Server Error: 404

+ + Back to Dashboard +
+
+ ) +}); +export default Page404; diff --git a/app/routes/IdealEnigma.tsx b/app/routes/IdealEnigma.tsx index 087f2297a7..e27f9b2ca5 100644 --- a/app/routes/IdealEnigma.tsx +++ b/app/routes/IdealEnigma.tsx @@ -1,5 +1,5 @@ -import { PageEnigma } from "../pages/PageEnigma"; +import { withProtectionRoute } from "~/modules/withProtectedRoute"; +import { PageEnigma } from "~/pages/PageEnigma"; -export default function IdealEnigma() { - return ; -} +const IdealEnigma = withProtectionRoute(()=>); +export default IdealEnigma; \ No newline at end of file diff --git a/app/routes/Login.tsx b/app/routes/Login.tsx index 4843521b2a..eb1205dcf3 100644 --- a/app/routes/Login.tsx +++ b/app/routes/Login.tsx @@ -1,29 +1,19 @@ import { useContext, useEffect, useState, useRef } from "react"; -import { useNavigate } from "@remix-run/react"; -import { useCookies } from 'react-cookie'; +import { useNavigate, useSearchParams } from "@remix-run/react"; import { faKey, faUser } from "@fortawesome/pro-solid-svg-icons"; - import { AuthenticationContext } from "~/contexts/Authentication"; -import { - Button, - H1, - Input, - Link, - P, -} from '~/components'; - +import { Button,H1,Input,Link,P } from '~/components'; import { LoadingDots } from "~/components"; export default function LoginScreen() { const navigate = useNavigate(); + const [searchParams] = useSearchParams(); const [showLoader, setShowLoader] = useState(undefined); const formRef = useRef(null); - const [authCookies, setAuthCookie, removeAuthCookie] = useCookies(['userInfo']); const {authState, loginAndGetUserInfo} = useContext(AuthenticationContext); - const handleOnSumbit = (ev: React.FormEvent)=>{ ev.preventDefault(); if(formRef.current){ @@ -43,8 +33,9 @@ export default function LoginScreen() { }// end handleOnSubmit useEffect(()=>{ + const redirectPath = searchParams.get('redirect'); if(authState && authState.isLoggedIn) - navigate("/"); + navigate(redirectPath ? redirectPath:'/'); },[authState]); return ( @@ -64,6 +55,7 @@ export default function LoginScreen() { icon={faUser} name="usernameOrEmail" placeholder="Username or Email" + autoComplete="username" required />