import { useCallback, useState, useMemo, useEffect, } from 'react' import { useHistory } from 'react-router' import type { User } from 'oidc-client' import isString from 'lodash/isString' import isBoolean from 'lodash/isBoolean' import { PAGES } from 'config' import { addLanguageUrlParam, writeToken, removeToken, readToken, setCookie, removeCookie, isMatchPage, TOKEN_KEY, } from 'helpers' import { useLocalStore, useSessionStore, useToggle, useEventListener, } from 'hooks' import { useLexicsStore } from 'features/LexicsStore' import { queryParamStorage } from 'features/QueryParamsStorage' import type { UserInfo, FailedResponse } from 'requests' import { getUserInfo, checkDevice, getTokenVirtualUser, } from 'requests' import { userManager } from '../config' import { needCheckNewDeviсe } from '../helpers' export const useAuth = () => { const { changeLang, lang } = useLexicsStore() const history = useHistory() const { close: markUserLoaded, isOpen: loadingUser, } = useToggle(true) const [user, setUser] = useState() const [isNewDeviceLogin, setIsNewDeviceLogin] = useState(false) const [userInfo, setUserInfo] = useState() const login = useCallback(async () => { userManager.signinRedirect({ extraQueryParams: { lang } }) }, [lang]) const logout = useCallback((key?: string) => { setPage(history.location.pathname) userManager.clearStaleState() userManager.createSigninRequest().then(({ url }) => { const urlWithLang = addLanguageUrlParam(lang, url) userManager.signoutRedirect({ post_logout_redirect_uri: urlWithLang }) }) removeToken() if (key !== 'saveToken') { removeCookie('access_token') } // eslint-disable-next-line react-hooks/exhaustive-deps }, [lang]) const storeUser = useCallback((loadedUser: User) => { setUser(loadedUser) writeToken(loadedUser.access_token) setCookie({ exdays: 1, name: 'access_token', value: loadedUser.access_token, }) }, []) const checkUser = useCallback(async () => { const loadedUser = await userManager.getUser() if (!loadedUser) { if (!readToken()) { const token = await getTemporaryToken() token && await fetchUserInfo() return Promise.resolve() } return Promise.reject() } storeUser(loadedUser) markUserLoaded() return loadedUser // eslint-disable-next-line react-hooks/exhaustive-deps }, [ storeUser, markUserLoaded, ]) const [page, setPage] = useLocalStore({ clearOnUnmount: true, defaultValue: '', key: 'matchBackLocation', validator: isString, }) const [search, setSearch] = useLocalStore({ clearOnUnmount: true, defaultValue: '', key: 'searchBack', validator: isString, }) const [isFromLanding, setIsFromLanding] = useSessionStore({ clearOnUnmount: true, defaultValue: false, key: 'isFromLanding', validator: isBoolean, }) useEffect(() => { if (isMatchPage()) setPage(history.location.pathname) if (history.location.pathname !== page) setIsFromLanding(false) }, [ history.location.pathname, page, setIsFromLanding, setPage, ]) const getTemporaryToken = async () => { try { const { access_token } = await getTokenVirtualUser() writeToken(access_token) setCookie({ exdays: 1, name: 'access_token', value: access_token, }) return access_token // eslint-disable-next-line no-empty } catch { return '' } } const signinRedirectCallback = useCallback(() => { userManager.signinRedirectCallback() .then((loadedUser) => { storeUser(loadedUser) queryParamStorage.clear() if (page.includes(PAGES.useraccount)) { history.push(PAGES.home) } else { const route = `${page}${search}` history.push(route) } markUserLoaded() setPage('') setSearch('') // } }).catch(login) // eslint-disable-next-line react-hooks/exhaustive-deps }, [ login, storeUser, markUserLoaded, ]) useEventListener({ callback: useCallback(async (e: StorageEvent) => { const loadedUser = await userManager.getUser() if ( e.storageArea !== localStorage || e.key !== TOKEN_KEY || !e.newValue || !loadedUser || loadedUser.access_token === e.newValue ) return userManager.storeUser({ ...loadedUser, access_token: e.newValue, toStorageString: loadedUser.toStorageString, }) }, []), event: 'storage', }) useEffect(() => { const isRedirectedBackFromAuthProvider = history.location.pathname === '/redirect' isRedirectedBackFromAuthProvider ? signinRedirectCallback() : checkUser() // eslint-disable-next-line react-hooks/exhaustive-deps }, [ checkUser, signinRedirectCallback, login, ]) const reChekNewDevice = useCallback(async () => { const loadedUser = await userManager.getUser() if (!loadedUser) return checkDevice(loadedUser.access_token).catch((er:FailedResponse) => { if (er.error) return if (isBoolean(er.ok) && !er.ok) { setIsNewDeviceLogin(true) setTimeout(logout, 10000) } }) }, [logout]) const checkNewDevice = useCallback(async () => { const loadedUser = await userManager.getUser() if (!loadedUser) return checkDevice(loadedUser.access_token).catch(() => { setTimeout(reChekNewDevice, 5000) }) }, [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()) } // если запросы вернули 401 | 403 window.addEventListener('FORBIDDEN_REQUEST', tryRenewToken) // и если токен истек userManager.events.addAccessTokenExpired(tryRenewToken) return () => { window.removeEventListener('FORBIDDEN_REQUEST', tryRenewToken) userManager.events.removeAccessTokenExpired(tryRenewToken) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [logout]) const fetchUserInfo = useCallback(async () => { try { const userInfoFetched = await getUserInfo() setUserInfo(userInfoFetched) userInfoFetched.language.iso && changeLang(userInfoFetched.language.iso) // eslint-disable-next-line no-empty } catch (error) {} }, [changeLang]) useEffect(() => { fetchUserInfo() }, [fetchUserInfo, user]) const auth = useMemo(() => ({ fetchUserInfo, isFromLanding, isNewDeviceLogin, loadingUser, login, logout, page, setIsFromLanding, setPage, setSearch, setUserInfo, user, userInfo, }), [ fetchUserInfo, isNewDeviceLogin, logout, user, userInfo, login, loadingUser, setSearch, setPage, setUserInfo, page, setIsFromLanding, isFromLanding, ]) return auth }