import { useCallback, useState, useMemo, useEffect, } from 'react' 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' import { PAGES } from 'config' import { addLanguageUrlParam, } from 'helpers/languageUrlParam' import { writeToken, removeToken, readToken, } from 'helpers/token' import { setCookie, removeCookie, } from 'helpers/cookie' import { useLocalStore, useToggle } 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 { getClientSettings, needCheckNewDeviсe } from '../helpers' import { getTokenVirtualUser } from '../../../requests' 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 userManager = useMemo(() => new UserManager(getClientSettings()), []) const login = useCallback(async () => { userManager.signinRedirect({ extraQueryParams: { lang } }) }, [userManager, 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 }, [userManager, 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 }, [ userManager, 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 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}${page === '/' ? search : ''}` history.push(route) } markUserLoaded() setPage('') setSearch('') // } }).catch(login) // eslint-disable-next-line react-hooks/exhaustive-deps }, [ userManager, login, storeUser, markUserLoaded, ]) 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, userManager]) const checkNewDevice = useCallback(async () => { const loadedUser = await userManager.getUser() if (!loadedUser) return checkDevice(loadedUser.access_token).catch(() => { setTimeout(reChekNewDevice, 2000) }) }, [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, ]) useEffect(() => { const tryRenewToken = () => { userManager.signinSilent() .catch(() => user && logout()) } // попытаемся обновить токен используя refresh_token // если запросы вернули 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 }, [userManager, logout]) useEffect(() => { // событие срабатывает после получения токена(первый // логин и обновление токена) userManager.events.addUserLoaded(storeUser) return () => userManager.events.removeUserLoaded(storeUser) }, [userManager, storeUser]) 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, isNewDeviceLogin, loadingUser, login, logout, setUserInfo, user, userInfo, }), [ fetchUserInfo, isNewDeviceLogin, logout, user, userInfo, login, loadingUser, setUserInfo, ]) return auth }