fix(web): refactor session context so it works more consistently

This commit is contained in:
41666 2021-03-14 18:19:05 -04:00
parent b6ae2abd2f
commit da9f0b6202
2 changed files with 165 additions and 71 deletions

View file

@ -2,16 +2,42 @@ import { SessionData } from '@roleypoly/types';
import * as React from 'react'; import * as React from 'react';
import { useApiContext } from '../api/ApiContext'; import { useApiContext } from '../api/ApiContext';
enum SessionState {
NoAuth,
HalfAuth,
FullAuth,
}
type SavedSession = {
sessionID: SessionData['sessionID'];
session: {
user: SessionData['user'];
guilds: SessionData['guilds'];
};
};
type SessionContextT = { type SessionContextT = {
session?: Omit<Partial<SessionData>, 'tokens'>; setupSession: (sessionID: string) => void;
setSession: (session?: SessionContextT['session']) => void;
authedFetch: (url: string, opts?: RequestInit) => Promise<Response>; authedFetch: (url: string, opts?: RequestInit) => Promise<Response>;
isAuthenticated: boolean; isAuthenticated: boolean;
sessionState: SessionState;
sessionID?: SessionData['sessionID'];
session: {
user?: SessionData['user'];
guilds?: SessionData['guilds'];
};
}; };
const SessionContext = React.createContext<SessionContextT>({ const SessionContext = React.createContext<SessionContextT>({
sessionState: SessionState.NoAuth,
sessionID: undefined,
isAuthenticated: false, isAuthenticated: false,
setSession: () => {}, session: {
user: undefined,
guilds: undefined,
},
setupSession: () => {},
authedFetch: async () => { authedFetch: async () => {
return new Response(); return new Response();
}, },
@ -20,84 +46,126 @@ const SessionContext = React.createContext<SessionContextT>({
export const useSessionContext = () => React.useContext(SessionContext); export const useSessionContext = () => React.useContext(SessionContext);
export const SessionContextProvider = (props: { children: React.ReactNode }) => { export const SessionContextProvider = (props: { children: React.ReactNode }) => {
const api = useApiContext(); const { fetch } = useApiContext();
const [session, setSession] = React.useState<SessionContextT['session']>(undefined); const [sessionID, setSessionID] = React.useState<SessionContextT['sessionID']>(
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 [sessionState, setSessionState] = React.useState<SessionState>(
SessionState.NoAuth
);
const [session, setSession] = React.useState<SessionContextT['session']>({
user: undefined,
guilds: undefined,
});
const [lock, setLock] = React.useState(false);
React.useEffect(() => { // Possible flows:
// No session is set, do we have one available? /*
if (!sessionContextValue.session || !sessionContextValue.session.sessionID) { if no key, check if key available in LS
// We may have the full state in session storage...
const storedSessionData = sessionStorage.getItem('rp_session_data'); No session:
if (storedSessionData) { no key
try { isAuth = false
setSession(JSON.parse(storedSessionData));
return; Half session:
} catch (e) { have key
// Oops, this data is wrong. 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<Response> => {
if (sessionID) {
init = {
...init,
headers: {
...init?.headers,
authorization: `Bearer ${sessionID}`,
},
};
} }
// But if not, we have the key, maybe? return fetch(url, init);
const storedSessionID = localStorage.getItem('rp_session_key'); },
if (storedSessionID && storedSessionID !== '') { };
setSession({ sessionID: storedSessionID });
const { setupSession, authedFetch } = sessionContextValue;
// Local storage sync on NoAuth
React.useEffect(() => {
if (!sessionID) {
const storedKey = getSessionKey();
if (!storedKey) {
return; return;
} }
// If we hit here, we're definitely not authenticated. setupSession(storedKey);
}
}, [sessionID, setupSession]);
// Sync session data on HalfAuth
React.useEffect(() => {
if (lock) {
return; return;
} }
// If a session is set and it's not stored, set it now. if (sessionState === SessionState.HalfAuth) {
if ( setLock(true);
localStorage.getItem('rp_session_key') !== sessionContextValue.session.sessionID
) {
localStorage.setItem('rp_session_key', sessionContextValue.session.sessionID || '');
}
// Session is set, but we don't have data. Server sup? // Use cached session
if (sessionContextValue.session.sessionID && !sessionContextValue.session.user) { 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 syncSession = async () => {
const response = await sessionContextValue.authedFetch('/get-session'); try {
if (response.status !== 200) { const serverSession = await fetchSession(authedFetch);
console.error('get-session failed', { response }); if (!serverSession) {
clearSessionData(); // Not found, lets reset.
return; 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(); syncSession();
} }
}, [ }, [sessionState, sessionID, authedFetch, lock]);
sessionContextValue.session?.user,
sessionContextValue.session?.sessionID,
sessionContextValue,
]);
return ( return (
<SessionContext.Provider value={sessionContextValue}> <SessionContext.Provider value={sessionContextValue}>
@ -106,7 +174,28 @@ export const SessionContextProvider = (props: { children: React.ReactNode }) =>
); );
}; };
const clearSessionData = () => { const saveSessionKey = (key: string) => localStorage.setItem('rp_session_key', key);
sessionStorage.removeItem('rp_session_data'); const deleteSessionKey = () => localStorage.removeItem('rp_session_key');
localStorage.removeItem('rp_session_key'); const getSessionKey = () => localStorage.getItem('rp_session_key');
type ServerSession = Omit<SessionData, 'tokens'>;
const fetchSession = async (
authedFetch: SessionContextT['authedFetch']
): Promise<ServerSession | null> => {
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');

View file

@ -5,7 +5,7 @@ import { useSessionContext } from '../../contexts/session/SessionContext';
import { Title } from '../../utils/metaTitle'; import { Title } from '../../utils/metaTitle';
const NewSession = (props: { sessionID: string }) => { const NewSession = (props: { sessionID: string }) => {
const session = useSessionContext(); const { setupSession, isAuthenticated } = useSessionContext();
const [postauthUrl, setPostauthUrl] = React.useState('/servers'); const [postauthUrl, setPostauthUrl] = React.useState('/servers');
React.useEffect(() => { React.useEffect(() => {
@ -13,7 +13,6 @@ const NewSession = (props: { sessionID: string }) => {
const id = props.sessionID || url.searchParams.get('session_id'); const id = props.sessionID || url.searchParams.get('session_id');
if (id) { if (id) {
localStorage.setItem('rp_session_key', id); localStorage.setItem('rp_session_key', id);
// session.setSession({ sessionID: id });
const storedPostauthUrl = localStorage.getItem('rp_postauth_redirect'); const storedPostauthUrl = localStorage.getItem('rp_postauth_redirect');
if (storedPostauthUrl) { if (storedPostauthUrl) {
@ -21,7 +20,13 @@ const NewSession = (props: { sessionID: string }) => {
localStorage.removeItem('rp_postauth_redirect'); localStorage.removeItem('rp_postauth_redirect');
} }
} }
}, [setPostauthUrl, props.sessionID, session]); }, [setPostauthUrl, props.sessionID]);
React.useEffect(() => {
if (props.sessionID) {
setupSession(props.sessionID);
}
}, [props.sessionID, setupSession]);
return ( return (
<> <>
@ -30,7 +35,7 @@ const NewSession = (props: { sessionID: string }) => {
<div> <div>
<Link href={postauthUrl}>If you aren't redirected soon, click here.</Link> <Link href={postauthUrl}>If you aren't redirected soon, click here.</Link>
</div> </div>
{session.isAuthenticated && <Redirect to={postauthUrl} noThrow replace />} {isAuthenticated && <Redirect to={postauthUrl} noThrow replace />}
</> </>
); );
}; };