fix(#3274): sets full user data from fetchFullUser instead of partial jwt data (#3279)

This commit is contained in:
Jarrod Flesch
2023-09-06 12:52:48 -04:00
committed by GitHub
parent 5096c37874
commit cf12b5fc70
6 changed files with 90 additions and 73 deletions

View File

@@ -1,5 +1,4 @@
import React, { createContext, useCallback, useContext, useEffect, useState } from 'react';
import jwtDecode from 'jwt-decode';
import { useHistory, useLocation } from 'react-router-dom';
import { useModal } from '@faceless-ui/modal';
import { useTranslation } from 'react-i18next';
@@ -17,6 +16,7 @@ const maxTimeoutTime = 2147483647;
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [user, setUser] = useState<User | null>();
const [tokenInMemory, setTokenInMemory] = useState<string>();
const [tokenExpiration, setTokenExpiration] = useState<number>();
const { pathname } = useLocation();
const { push } = useHistory();
@@ -35,7 +35,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
},
} = config;
const exp = user?.exp;
const exp = tokenExpiration;
const [permissions, setPermissions] = useState<Permissions>();
@@ -56,9 +56,24 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
closeAllModals();
}, [push, admin, logoutInactivityRoute, closeAllModals]);
const revokeTokenAndExpire = useCallback(() => {
setTokenInMemory(undefined);
setTokenExpiration(undefined);
}, []);
const setTokenAndExpiration = useCallback((json) => {
const token = json?.token || json?.refreshedToken;
if (token && json?.exp) {
setTokenInMemory(token);
setTokenExpiration(json.exp);
} else {
revokeTokenAndExpire();
}
}, [revokeTokenAndExpire]);
const refreshCookie = useCallback((forceRefresh?: boolean) => {
const now = Math.round((new Date()).getTime() / 1000);
const remainingTime = (exp as number || 0) - now;
const remainingTime = (typeof exp === 'number' ? exp : 0) - now;
if (forceRefresh || (exp && remainingTime < 120)) {
setTimeout(async () => {
@@ -72,6 +87,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
if (request.status === 200) {
const json = await request.json();
setUser(json.user);
setTokenAndExpiration(json);
} else {
setUser(null);
redirectToInactivityRoute();
@@ -81,7 +97,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
}
}, 1000);
}
}, [exp, serverURL, api, userSlug, i18n, redirectToInactivityRoute]);
}, [serverURL, api, userSlug, i18n, exp, redirectToInactivityRoute, setTokenAndExpiration]);
const refreshCookieAsync = useCallback(async (skipSetUser?: boolean): Promise<User> => {
try {
@@ -93,7 +109,10 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
if (request.status === 200) {
const json = await request.json();
if (!skipSetUser) setUser(json.user);
if (!skipSetUser) {
setUser(json.user);
setTokenAndExpiration(json);
}
return json.user;
}
@@ -104,19 +123,13 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
toast.error(`Refreshing token failed: ${e.message}`);
return null;
}
}, [serverURL, api, userSlug, i18n, redirectToInactivityRoute]);
const setToken = useCallback((token: string) => {
const decoded = jwtDecode<User>(token);
setUser(decoded);
setTokenInMemory(token);
}, []);
}, [serverURL, api, userSlug, i18n, redirectToInactivityRoute, setTokenAndExpiration]);
const logOut = useCallback(() => {
setUser(null);
setTokenInMemory(undefined);
revokeTokenAndExpire();
requests.post(`${serverURL}${api}/${userSlug}/logout`);
}, [serverURL, api, userSlug]);
}, [serverURL, api, userSlug, revokeTokenAndExpire]);
const refreshPermissions = useCallback(async () => {
try {
@@ -137,56 +150,59 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
}
}, [serverURL, api, i18n]);
// On mount, get user and set
useEffect(() => {
const fetchMe = async () => {
try {
const request = await requests.get(`${serverURL}${api}/${userSlug}/me`, {
headers: {
'Accept-Language': i18n.language,
},
});
const fetchFullUser = React.useCallback(async () => {
try {
const request = await requests.get(`${serverURL}${api}/${userSlug}/me`, {
headers: {
'Accept-Language': i18n.language,
},
});
if (request.status === 200) {
const json = await request.json();
if (request.status === 200) {
const json = await request.json();
if (json?.user) {
setUser(json.user);
} else if (json?.token) {
setToken(json.token);
} else if (autoLogin && autoLogin.prefillOnly !== true) {
// auto log-in with the provided autoLogin credentials. This is used in dev mode
// so you don't have to log in over and over again
const autoLoginResult = await requests.post(`${serverURL}${api}/${userSlug}/login`, {
body: JSON.stringify({
email: autoLogin.email,
password: autoLogin.password,
}),
headers: {
'Accept-Language': i18n.language,
'Content-Type': 'application/json',
},
});
if (autoLoginResult.status === 200) {
const autoLoginJson = await autoLoginResult.json();
setUser(autoLoginJson.user);
if (autoLoginJson?.token) {
setToken(autoLoginJson.token);
}
} else {
setUser(null);
if (json?.user) {
setUser(json.user);
if (json?.token) {
setTokenAndExpiration(json);
}
} else if (autoLogin && autoLogin.prefillOnly !== true) {
// auto log-in with the provided autoLogin credentials. This is used in dev mode
// so you don't have to log in over and over again
const autoLoginResult = await requests.post(`${serverURL}${api}/${userSlug}/login`, {
body: JSON.stringify({
email: autoLogin.email,
password: autoLogin.password,
}),
headers: {
'Accept-Language': i18n.language,
'Content-Type': 'application/json',
},
});
if (autoLoginResult.status === 200) {
const autoLoginJson = await autoLoginResult.json();
setUser(autoLoginJson.user);
if (autoLoginJson?.token) {
setTokenAndExpiration(autoLoginJson);
}
} else {
setUser(null);
revokeTokenAndExpire();
}
} else {
setUser(null);
revokeTokenAndExpire();
}
} catch (e) {
toast.error(`Fetching user failed: ${e.message}`);
}
};
} catch (e) {
toast.error(`Fetching user failed: ${e.message}`);
}
}, [serverURL, api, userSlug, i18n, autoLogin, setTokenAndExpiration, revokeTokenAndExpire]);
fetchMe();
}, [i18n, setToken, api, serverURL, userSlug, autoLogin]);
// On mount, get user and set
useEffect(() => {
fetchFullUser();
}, [fetchFullUser]);
// When location changes, refresh cookie
useEffect(() => {
@@ -209,12 +225,12 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
useEffect(() => {
let reminder: ReturnType<typeof setTimeout>;
const now = Math.round((new Date()).getTime() / 1000);
const remainingTime = exp as number - now;
const remainingTime = typeof exp === 'number' ? exp - now : 0;
if (remainingTime > 0) {
reminder = setTimeout(() => {
openModal('stay-logged-in');
}, (Math.min((remainingTime - 60) * 1000), maxTimeoutTime));
}, Math.max(Math.min((remainingTime - 60) * 1000, maxTimeoutTime)));
}
return () => {
@@ -225,19 +241,20 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
useEffect(() => {
let forceLogOut: ReturnType<typeof setTimeout>;
const now = Math.round((new Date()).getTime() / 1000);
const remainingTime = exp as number - now;
const remainingTime = typeof exp === 'number' ? exp - now : 0;
if (remainingTime > 0) {
forceLogOut = setTimeout(() => {
setUser(null);
revokeTokenAndExpire();
redirectToInactivityRoute();
}, Math.min(remainingTime * 1000, maxTimeoutTime));
}, Math.max(Math.min(remainingTime * 1000, maxTimeoutTime), 0));
}
return () => {
if (forceLogOut) clearTimeout(forceLogOut);
};
}, [exp, closeAllModals, i18n, redirectToInactivityRoute]);
}, [exp, closeAllModals, i18n, redirectToInactivityRoute, revokeTokenAndExpire]);
return (
<Context.Provider value={{
@@ -248,8 +265,8 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
refreshCookieAsync,
refreshPermissions,
permissions,
setToken,
token: tokenInMemory,
fetchFullUser,
}}
>
{children}

View File

@@ -6,8 +6,8 @@ export type AuthContext<T = User> = {
logOut: () => void
refreshCookie: (forceRefresh?: boolean) => void
refreshCookieAsync: () => Promise<User>
setToken: (token: string) => void
token?: string
refreshPermissions: () => Promise<void>
permissions?: Permissions
fetchFullUser: () => Promise<void>
}

View File

@@ -17,7 +17,7 @@ const baseClass = 'create-first-user';
const CreateFirstUser: React.FC<Props> = (props) => {
const { setInitialized } = props;
const { setToken } = useAuth();
const { fetchFullUser } = useAuth();
const {
admin: { user: userSlug }, collections, serverURL, routes: { admin, api },
} = useConfig();
@@ -25,9 +25,9 @@ const CreateFirstUser: React.FC<Props> = (props) => {
const userConfig = collections.find((collection) => collection.slug === userSlug);
const onSuccess = (json) => {
const onSuccess = async (json) => {
if (json?.user?.token) {
setToken(json.user.token);
await fetchFullUser();
}
setInitialized(true);

View File

@@ -20,7 +20,7 @@ const baseClass = 'login';
const Login: React.FC = () => {
const history = useHistory();
const { t } = useTranslation('authentication');
const { user, setToken } = useAuth();
const { user, fetchFullUser } = useAuth();
const config = useConfig();
const {
admin: {
@@ -47,9 +47,9 @@ const Login: React.FC = () => {
const redirect = query.get('redirect');
const onSuccess = (data) => {
const onSuccess = async (data) => {
if (data.token) {
setToken(data.token);
await fetchFullUser();
// Ensure the redirect always starts with the admin route, and concatenate the redirect path
history.push(admin + (redirect || ''));

View File

@@ -22,12 +22,12 @@ const ResetPassword: React.FC = () => {
const { admin: { user: userSlug, logoutRoute }, serverURL, routes: { admin, api } } = config;
const { token } = useParams<{ token?: string }>();
const history = useHistory();
const { user, setToken } = useAuth();
const { user, fetchFullUser } = useAuth();
const { t } = useTranslation('authentication');
const onSuccess = (data) => {
const onSuccess = async (data) => {
if (data.token) {
setToken(data.token);
await fetchFullUser();
history.push(`${admin}`);
} else {
history.push(`${admin}/login`);