|
|
|
|
@@ -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}
|
|
|
|
|
|