From da9f0b62022b3d3a26b9645f1c4daa4442072b58 Mon Sep 17 00:00:00 2001 From: Katalina Okano Date: Sun, 14 Mar 2021 18:19:05 -0400 Subject: [PATCH] fix(web): refactor session context so it works more consistently --- .../src/contexts/session/SessionContext.tsx | 223 ++++++++++++------ .../web/src/pages/machinery/new-session.tsx | 13 +- 2 files changed, 165 insertions(+), 71 deletions(-) diff --git a/packages/web/src/contexts/session/SessionContext.tsx b/packages/web/src/contexts/session/SessionContext.tsx index 84a6dd5..e3c7cdb 100644 --- a/packages/web/src/contexts/session/SessionContext.tsx +++ b/packages/web/src/contexts/session/SessionContext.tsx @@ -2,16 +2,42 @@ import { SessionData } from '@roleypoly/types'; import * as React from 'react'; import { useApiContext } from '../api/ApiContext'; +enum SessionState { + NoAuth, + HalfAuth, + FullAuth, +} + +type SavedSession = { + sessionID: SessionData['sessionID']; + + session: { + user: SessionData['user']; + guilds: SessionData['guilds']; + }; +}; + type SessionContextT = { - session?: Omit, 'tokens'>; - setSession: (session?: SessionContextT['session']) => void; + setupSession: (sessionID: string) => void; authedFetch: (url: string, opts?: RequestInit) => Promise; isAuthenticated: boolean; + sessionState: SessionState; + sessionID?: SessionData['sessionID']; + session: { + user?: SessionData['user']; + guilds?: SessionData['guilds']; + }; }; const SessionContext = React.createContext({ + sessionState: SessionState.NoAuth, + sessionID: undefined, isAuthenticated: false, - setSession: () => {}, + session: { + user: undefined, + guilds: undefined, + }, + setupSession: () => {}, authedFetch: async () => { return new Response(); }, @@ -20,84 +46,126 @@ const SessionContext = React.createContext({ export const useSessionContext = () => React.useContext(SessionContext); export const SessionContextProvider = (props: { children: React.ReactNode }) => { - const api = useApiContext(); - const [session, setSession] = React.useState(undefined); - - const sessionContextValue: SessionContextT = React.useMemo( - () => ({ - session, - setSession, - authedFetch: (url: string, opts?: RequestInit) => { - return api.fetch(url, { - ...opts, - headers: { - ...opts?.headers, - authorization: session?.sessionID - ? `Bearer ${session?.sessionID}` - : undefined, - }, - }); - }, - isAuthenticated: !!session && !!session.sessionID && !!session.user, - }), - [session, api] + const { fetch } = useApiContext(); + const [sessionID, setSessionID] = React.useState( + undefined ); + const [sessionState, setSessionState] = React.useState( + SessionState.NoAuth + ); + const [session, setSession] = React.useState({ + user: undefined, + guilds: undefined, + }); + const [lock, setLock] = React.useState(false); - React.useEffect(() => { - // No session is set, do we have one available? - if (!sessionContextValue.session || !sessionContextValue.session.sessionID) { - // We may have the full state in session storage... - const storedSessionData = sessionStorage.getItem('rp_session_data'); - if (storedSessionData) { - try { - setSession(JSON.parse(storedSessionData)); - return; - } catch (e) { - // Oops, this data is wrong. - } + // Possible flows: + /* + if no key, check if key available in LS + + No session: + no key + isAuth = false + + Half session: + have key + lock = true + isAuth = false + fetch cached in SS _OR_ syncSession() + lock = false + + Full session + have session + isAuth = true + */ + + const sessionContextValue: SessionContextT = { + sessionID, + session, + sessionState, + isAuthenticated: sessionState === SessionState.FullAuth, + setupSession: async (newID: string) => { + setSessionID(newID); + setSessionState(SessionState.HalfAuth); + saveSessionKey(newID); + }, + authedFetch: async (url: string, init?: RequestInit): Promise => { + if (sessionID) { + init = { + ...init, + headers: { + ...init?.headers, + authorization: `Bearer ${sessionID}`, + }, + }; } - // But if not, we have the key, maybe? - const storedSessionID = localStorage.getItem('rp_session_key'); - if (storedSessionID && storedSessionID !== '') { - setSession({ sessionID: storedSessionID }); + return fetch(url, init); + }, + }; + + const { setupSession, authedFetch } = sessionContextValue; + + // Local storage sync on NoAuth + React.useEffect(() => { + if (!sessionID) { + const storedKey = getSessionKey(); + if (!storedKey) { return; } - // If we hit here, we're definitely not authenticated. + setupSession(storedKey); + } + }, [sessionID, setupSession]); + + // Sync session data on HalfAuth + React.useEffect(() => { + if (lock) { return; } - // If a session is set and it's not stored, set it now. - if ( - localStorage.getItem('rp_session_key') !== sessionContextValue.session.sessionID - ) { - localStorage.setItem('rp_session_key', sessionContextValue.session.sessionID || ''); - } + if (sessionState === SessionState.HalfAuth) { + setLock(true); - // Session is set, but we don't have data. Server sup? - if (sessionContextValue.session.sessionID && !sessionContextValue.session.user) { + // Use cached session + const storedData = getSessionData(); + if (storedData && storedData?.sessionID === sessionID) { + setSession(storedData.session); + setSessionState(SessionState.FullAuth); + setLock(false); + return; + } + + // If no cached session, let's grab it from server const syncSession = async () => { - const response = await sessionContextValue.authedFetch('/get-session'); - if (response.status !== 200) { - console.error('get-session failed', { response }); - clearSessionData(); - return; + try { + const serverSession = await fetchSession(authedFetch); + if (!serverSession) { + // Not found, lets reset. + deleteSessionKey(); + setSessionID(undefined); + setSessionState(SessionState.NoAuth); + setLock(false); + return; + } + + const newSession = { + user: serverSession.user, + guilds: serverSession.guilds, + }; + + saveSessionData({ sessionID: sessionID || '', session: newSession }); + setSession(newSession); + setLock(false); + } catch (e) { + console.error('syncSession failed', e); + setLock(false); } - - const serverSession: SessionContextT['session'] = await response.json(); - - setSession(serverSession); - sessionStorage.setItem('rp_session_data', JSON.stringify(serverSession)); }; syncSession(); } - }, [ - sessionContextValue.session?.user, - sessionContextValue.session?.sessionID, - sessionContextValue, - ]); + }, [sessionState, sessionID, authedFetch, lock]); return ( @@ -106,7 +174,28 @@ export const SessionContextProvider = (props: { children: React.ReactNode }) => ); }; -const clearSessionData = () => { - sessionStorage.removeItem('rp_session_data'); - localStorage.removeItem('rp_session_key'); +const saveSessionKey = (key: string) => localStorage.setItem('rp_session_key', key); +const deleteSessionKey = () => localStorage.removeItem('rp_session_key'); +const getSessionKey = () => localStorage.getItem('rp_session_key'); + +type ServerSession = Omit; +const fetchSession = async ( + authedFetch: SessionContextT['authedFetch'] +): Promise => { + const sessionResponse = await authedFetch('/get-session'); + if (sessionResponse.status !== 200) { + return null; + } + + const { sessionID, guilds, user }: ServerSession = await sessionResponse.json(); + return { + sessionID, + guilds, + user, + }; }; + +const saveSessionData = (data: SavedSession) => + sessionStorage.setItem('rp_session_data', JSON.stringify(data)); +const getSessionData = (): SavedSession | null => + JSON.parse(sessionStorage.getItem('rp_session_data') || 'null'); diff --git a/packages/web/src/pages/machinery/new-session.tsx b/packages/web/src/pages/machinery/new-session.tsx index 0e47224..af950f6 100644 --- a/packages/web/src/pages/machinery/new-session.tsx +++ b/packages/web/src/pages/machinery/new-session.tsx @@ -5,7 +5,7 @@ import { useSessionContext } from '../../contexts/session/SessionContext'; import { Title } from '../../utils/metaTitle'; const NewSession = (props: { sessionID: string }) => { - const session = useSessionContext(); + const { setupSession, isAuthenticated } = useSessionContext(); const [postauthUrl, setPostauthUrl] = React.useState('/servers'); React.useEffect(() => { @@ -13,7 +13,6 @@ const NewSession = (props: { sessionID: string }) => { const id = props.sessionID || url.searchParams.get('session_id'); if (id) { localStorage.setItem('rp_session_key', id); - // session.setSession({ sessionID: id }); const storedPostauthUrl = localStorage.getItem('rp_postauth_redirect'); if (storedPostauthUrl) { @@ -21,7 +20,13 @@ const NewSession = (props: { sessionID: string }) => { localStorage.removeItem('rp_postauth_redirect'); } } - }, [setPostauthUrl, props.sessionID, session]); + }, [setPostauthUrl, props.sessionID]); + + React.useEffect(() => { + if (props.sessionID) { + setupSession(props.sessionID); + } + }, [props.sessionID, setupSession]); return ( <> @@ -30,7 +35,7 @@ const NewSession = (props: { sessionID: string }) => {
If you aren't redirected soon, click here.
- {session.isAuthenticated && } + {isAuthenticated && } ); };