import { useCallback, useState, useMemo, useEffect, } from 'react' import { useHistory } from 'react-router' import type { User } from 'oidc-client' // @ts-expect-error import duel from 'dueljs' import isString from 'lodash/isString' import isBoolean from 'lodash/isBoolean' import includes from 'lodash/includes' import { PAGES } from 'config' import { addLanguageUrlParam, writeToken, removeToken, readToken, setCookie, removeCookie, } from 'helpers' import { useLocalStore, useSessionStore, useToggle, } 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 saveToken = (value: string) => { writeToken(value) setCookie({ exdays: 1, name: 'access_token', value, }) } 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) saveToken(loadedUser.access_token) }, []) const checkUser = useCallback(async () => { const loadedUser = await userManager.getUser() if (!loadedUser) { if (history.location.pathname.includes(PAGES.mailings)) { const url = new URL(window.location.href) const access_token = url.searchParams.get('access_token') || '' saveToken(access_token) access_token && await fetchUserInfo() return Promise.resolve() } if (!readToken()) { const token = await getTemporaryToken() token && await fetchUserInfo() return Promise.resolve() } return Promise.resolve() } 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, }) const [landingUrlFrom, setLandingUrlFrom] = useSessionStore({ clearOnUnmount: true, defaultValue: '', key: 'landingUrlFrom', validator: isString, }) const getTemporaryToken = async () => { try { const { access_token } = await getTokenVirtualUser() saveToken(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, ]) 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, ]) duel.channel('active_page') // поле в LS, определяющее активность вкладки useEffect(() => { // попытаемся обновить токен используя refresh_token const tryRenewToken = () => { // библиотека oidc-client не поддерживает обновление токена только на 1 вкладке // @ts-ignore if (window.isMaster()) { userManager.signinSilent().catch(logout) } } // если запросы вернули 401 | 403 window.addEventListener('FORBIDDEN_REQUEST', tryRenewToken) // и если токен истекает (по дефолту за 60 секунд) userManager.events.addAccessTokenExpiring(tryRenewToken) return () => { window.removeEventListener('FORBIDDEN_REQUEST', tryRenewToken) userManager.events.removeAccessTokenExpiring(tryRenewToken) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [logout]) useEffect(() => { // событие срабатывает после получения токена(первый // логин и обновление токена) userManager.events.addUserLoaded(storeUser) return () => userManager.events.removeUserLoaded(storeUser) // eslint-disable-next-line react-hooks/exhaustive-deps }, [userManager, storeUser]) const fetchUserInfo = useCallback(async () => { try { const userInfoFetched = await getUserInfo() setUserInfo(userInfoFetched) if (includes(window.location.pathname, PAGES.landing) && readToken()) { changeLang(navigator.language.substring(0, 2)) } else { userInfoFetched.language.iso && changeLang(userInfoFetched.language.iso) } // eslint-disable-next-line no-empty } catch (error) {} }, [changeLang]) useEffect(() => { readToken() && fetchUserInfo() }, [fetchUserInfo, user]) const auth = useMemo(() => ({ fetchUserInfo, isFromLanding, isNewDeviceLogin, landingUrlFrom, loadingUser, login, logout, page, setIsFromLanding, setLandingUrlFrom, setPage, setSearch, setUserInfo, user, userInfo, }), [ fetchUserInfo, isNewDeviceLogin, logout, user, userInfo, landingUrlFrom, login, loadingUser, setSearch, setPage, setUserInfo, page, setIsFromLanding, setLandingUrlFrom, isFromLanding, ]) return auth }