protected routes, 404 page, auth with localstorage, login redirects

This commit is contained in:
wil committed 2024-04-04 16:10:14 -04:00
1 parent 207a5394f5
commit e183e60fe2
6 files changed
+141 -61

No files matched your search

+73 -42
View File
@@ -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<SetStateAction<AuthState | undefined>>,
persistLoginOrOut?: ()=>void,
authState: AuthState,
setAuthState?: Dispatch<SetStateAction<AuthState>>,
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<AuthState | undefined>(undefined);
console.log('auth provider rerender');
const [authCookies, setAuthCookie, removeAuthCookie] = useCookies([STORAGE_KEYS.USER_INFO]);
const [authState, setAuthState] = useState<AuthState>({});
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(
<AuthenticationContext.Provider value={{
authState,
setAuthState,
persistLoginOrOut,
loginAndGetUserInfo,
logout,
}}>
+5 -1
View File
@@ -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 {
+26
View File
@@ -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 = <P extends {}>(Component: ComponentType<P>) => (rest:P) => {
const { authState } = useContext(AuthenticationContext);
console.log(authState);
if (authState.isLoggedIn === undefined ) {
return <>waiting</>;
}
if (authState.isLoggedIn === false) {
return <RedirectToLogin />
}
return (
<Component {...rest}/>
);
}
const RedirectToLogin = ()=>{
const navigate = useNavigate();
const {pathname} = useLocation();
useEffect(()=>{
navigate(`/login?redirect=${pathname}`);
},[]);
return <>redirecting</>
}
+27
View File
@@ -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(
<div
className="fixed w-full overflow-scroll"
style={{height: "calc(100% - 72px)"}}
>
<div
className='bg-ui-panel w-10/12 max-w-7xl h-1/2 mx-auto my-24 rounded-lg p-6 flex flex-col items-center justify-center gap-8 text-center'
>
<P>Oops, seems like this is not a page that exists!</P>
<H1>Server Error: 404</H1>
<ButtonLink to="/" icon={faChevronLeft}>Back to Dashboard</ButtonLink>
</div>
</div>
)
});
export default Page404;
+4 -4
View File
@@ -1,5 +1,5 @@
import { PageEnigma } from "../pages/PageEnigma";
import { withProtectionRoute } from "~/modules/withProtectedRoute";
import { PageEnigma } from "~/pages/PageEnigma";
export default function IdealEnigma() {
return <PageEnigma />;
}
const IdealEnigma = withProtectionRoute(()=><PageEnigma />);
export default IdealEnigma;
+6 -14
View File
@@ -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<string|undefined>(undefined);
const formRef = useRef<HTMLFormElement | null>(null);
const [authCookies, setAuthCookie, removeAuthCookie] = useCookies(['userInfo']);
const {authState, loginAndGetUserInfo} = useContext(AuthenticationContext);
const handleOnSumbit = (ev: React.FormEvent<HTMLFormElement>)=>{
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
/>
<br />