diff --git a/public/silent-refresh.html b/public/silent-refresh.html deleted file mode 100644 index 852cc138..00000000 --- a/public/silent-refresh.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - diff --git a/src/features/AuthStore/config.tsx b/src/features/AuthStore/config.tsx new file mode 100644 index 00000000..61072f62 --- /dev/null +++ b/src/features/AuthStore/config.tsx @@ -0,0 +1,7 @@ +import { UserManager } from 'oidc-client' + +import { getClientSettings } from './helpers' + +export const userManager = new UserManager(getClientSettings()) + +export const channel = new BroadcastChannel('access_token') diff --git a/src/features/AuthStore/helpers.tsx b/src/features/AuthStore/helpers.tsx index 50818c4f..e938180b 100644 --- a/src/features/AuthStore/helpers.tsx +++ b/src/features/AuthStore/helpers.tsx @@ -61,7 +61,6 @@ const redirectUrl = () => { export const getClientSettings = (): Settings => ({ authority: AUTH_SERVICE, - automaticSilentRenew: true, client_id: client.auth.clientId, filterProtocolClaims: false, loadUserInfo: false, @@ -70,7 +69,6 @@ export const getClientSettings = (): Settings => ({ response_mode: 'query', response_type: 'id_token token', scope: 'openid', - silent_redirect_uri: `${window.location.origin ?? window.origin}/silent-refresh.html`, userStore: new WebStorageStateStore({ store: window.localStorage }), }) diff --git a/src/features/AuthStore/hooks/useAuth.tsx b/src/features/AuthStore/hooks/useAuth.tsx index 9025b74e..92f994c7 100644 --- a/src/features/AuthStore/hooks/useAuth.tsx +++ b/src/features/AuthStore/hooks/useAuth.tsx @@ -7,7 +7,6 @@ import { import { useHistory } from 'react-router' import type { User } from 'oidc-client' -import { UserManager } from 'oidc-client' import isString from 'lodash/isString' import isBoolean from 'lodash/isBoolean' @@ -16,34 +15,33 @@ import { PAGES } from 'config' import { addLanguageUrlParam, -} from 'helpers/languageUrlParam' - -import { writeToken, removeToken, readToken, -} from 'helpers/token' -import { setCookie, removeCookie, -} from 'helpers/cookie' -import { isMatchPage } from 'helpers/isMatchPage' + isMatchPage, +} from 'helpers' import { useLocalStore, useSessionStore, useToggle, + useEventListener, } from 'hooks' import { useLexicsStore } from 'features/LexicsStore' import { queryParamStorage } from 'features/QueryParamsStorage' -import { getUserInfo, UserInfo } from 'requests/getUserInfo' -import { checkDevice, FailedResponse } from 'requests/checkDevice' -import { getTokenVirtualUser } from 'requests/getTokenVirtualUser' +import type { UserInfo, FailedResponse } from 'requests' +import { + getUserInfo, + checkDevice, + getTokenVirtualUser, +} from 'requests' -// eslint-disable-next-line -import { getClientSettings, needCheckNewDeviсe } from '../helpers' +import { userManager, channel } from '../config' +import { needCheckNewDeviсe } from '../helpers' export const useAuth = () => { const { changeLang, lang } = useLexicsStore() @@ -55,11 +53,10 @@ export const useAuth = () => { const [user, setUser] = useState() const [isNewDeviceLogin, setIsNewDeviceLogin] = useState(false) const [userInfo, setUserInfo] = useState() - const userManager = useMemo(() => new UserManager(getClientSettings()), []) const login = useCallback(async () => { userManager.signinRedirect({ extraQueryParams: { lang } }) - }, [userManager, lang]) + }, [lang]) const logout = useCallback((key?: string) => { setPage(history.location.pathname) @@ -73,7 +70,7 @@ export const useAuth = () => { removeCookie('access_token') } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [userManager, lang]) + }, [lang]) const storeUser = useCallback((loadedUser: User) => { setUser(loadedUser) @@ -83,6 +80,8 @@ export const useAuth = () => { name: 'access_token', value: loadedUser.access_token, }) + // обновляем токен в других окнах/вкладках + channel.postMessage({ token: loadedUser.access_token }) }, []) const checkUser = useCallback(async () => { @@ -102,7 +101,6 @@ export const useAuth = () => { return loadedUser // eslint-disable-next-line react-hooks/exhaustive-deps }, [ - userManager, storeUser, markUserLoaded, ]) @@ -173,12 +171,31 @@ export const useAuth = () => { }).catch(login) // eslint-disable-next-line react-hooks/exhaustive-deps }, [ - userManager, login, storeUser, markUserLoaded, ]) + useEventListener({ + callback: useCallback(async (e: MessageEvent<{ token: string }>) => { + const loadedUser = await userManager.getUser() + + if ( + !e.data.token + || !loadedUser + || loadedUser.access_token === e.data.token + ) return + + userManager.storeUser({ + ...loadedUser, + access_token: e.data.token, + toStorageString: loadedUser.toStorageString, + }) + }, []), + event: 'message', + target: channel, + }) + useEffect(() => { const isRedirectedBackFromAuthProvider = history.location.pathname === '/redirect' isRedirectedBackFromAuthProvider ? signinRedirectCallback() : checkUser() @@ -200,34 +217,41 @@ export const useAuth = () => { setTimeout(logout, 10000) } }) - }, [logout, userManager]) + }, [logout]) - // eslint-disable-next-line const checkNewDevice = useCallback(async () => { const loadedUser = await userManager.getUser() if (!loadedUser) return checkDevice(loadedUser.access_token).catch(() => { - setTimeout(reChekNewDevice, 2000) + setTimeout(reChekNewDevice, 5000) }) - }, [reChekNewDevice, userManager]) - - // useEffect(() => { - // if (!needCheckNewDeviсe && !user) return undefined - // const startCheckDevice = setInterval(checkNewDevice, 20000) - // isNewDeviceLogin && clearInterval(startCheckDevice) - // return () => clearInterval(startCheckDevice) - // - // // eslint-disable-next-line react-hooks/exhaustive-deps - // }, [ - // checkNewDevice, - // isNewDeviceLogin, - // setIsNewDeviceLogin, - // ]) + }, [reChekNewDevice]) + + useEffect(() => { + if (!needCheckNewDeviсe && !user) return undefined + const startCheckDevice = setInterval(checkNewDevice, 20000) + isNewDeviceLogin && clearInterval(startCheckDevice) + return () => clearInterval(startCheckDevice) + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + checkNewDevice, + isNewDeviceLogin, + setIsNewDeviceLogin, + ]) useEffect(() => { // попытаемся обновить токен используя refresh_token const tryRenewToken = () => { + const tokenLastUpdated = Number(localStorage.getItem('token_updated')) + // предотвращаем одновременное обновление токена в разных окнах/вкладках + const needRenewToken = Date.now() - tokenLastUpdated >= userManager.settings.clockSkew! * 1e3 + + if (!needRenewToken) return + + localStorage.setItem('token_updated', String(Date.now())) + userManager.signinSilent() .catch(() => user && logout()) } @@ -241,14 +265,7 @@ export const useAuth = () => { userManager.events.removeAccessTokenExpired(tryRenewToken) } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [userManager, logout]) - - useEffect(() => { - // событие срабатывает после получения токена(первый - // логин и обновление токена) - userManager.events.addUserLoaded(storeUser) - return () => userManager.events.removeUserLoaded(storeUser) - }, [userManager, storeUser]) + }, [logout]) const fetchUserInfo = useCallback(async () => { try { diff --git a/src/helpers/index.tsx b/src/helpers/index.tsx index f1e5376d..ce271b5d 100644 --- a/src/helpers/index.tsx +++ b/src/helpers/index.tsx @@ -11,3 +11,6 @@ export * from './selectedApi' export * from './openSubscribePopup' export * from './getCurrentYear' export * from './getTeamAbbr' +export * from './cookie' +export * from './isMatchPage' +export * from './languageUrlParam' diff --git a/src/hooks/useEventListener.tsx b/src/hooks/useEventListener.tsx index 3d0e4680..4325e25c 100644 --- a/src/hooks/useEventListener.tsx +++ b/src/hooks/useEventListener.tsx @@ -4,7 +4,7 @@ import { useEffect, useRef } from 'react' type EventMap = HTMLElementEventMap & WindowEventMap -type Target = RefObject | HTMLElement | Window +type Target = RefObject | HTMLElement | Window | BroadcastChannel type Args = { callback: (e: EventMap[E]) => void, @@ -28,7 +28,7 @@ export const useEventListener = ({ }, [callback]) useEffect(() => { - const windowOrElement: HTMLElement | Window | null = 'current' in target + const windowOrElement: HTMLElement | Window | BroadcastChannel | null = 'current' in target ? target.current : target diff --git a/src/requests/index.tsx b/src/requests/index.tsx index d96b03e2..494e9a3a 100644 --- a/src/requests/index.tsx +++ b/src/requests/index.tsx @@ -34,3 +34,5 @@ export * from './getTeamsStats' export * from './getPlayersStats' export * from './getMatchParticipants' export * from './getStatsEvents' +export * from './getTokenVirtualUser' +export * from './checkDevice'