diff --git a/package.json b/package.json
index f78ae89f..574a224e 100644
--- a/package.json
+++ b/package.json
@@ -15,10 +15,12 @@
"dependencies": {
"@stripe/react-stripe-js": "^1.4.0",
"@stripe/stripe-js": "^1.13.2",
+ "babel-polyfill": "^6.26.0",
"date-fns": "^2.14.0",
"history": "^4.10.1",
"hls.js": "^0.14.15",
"lodash": "^4.17.15",
+ "oidc-client": "^1.11.5",
"react": "^17.0.1",
"react-datepicker": "^3.1.3",
"react-dom": "^17.0.1",
diff --git a/public/silent-refresh.html b/public/silent-refresh.html
new file mode 100644
index 00000000..852cc138
--- /dev/null
+++ b/public/silent-refresh.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/config/authKeys.tsx b/src/config/authKeys.tsx
index 74db72ec..2ababeaf 100644
--- a/src/config/authKeys.tsx
+++ b/src/config/authKeys.tsx
@@ -1 +1 @@
-export const TOKEN_KEY = 'x-auth-token'
+export const TOKEN_KEY = 'Authorization'
diff --git a/src/config/routes.tsx b/src/config/routes.tsx
index ea2e231e..897c6015 100644
--- a/src/config/routes.tsx
+++ b/src/config/routes.tsx
@@ -1,4 +1,2 @@
-import { isProduction } from './env'
-
-export const API_ROOT = isProduction ? 'https://api.instat.tv' : 'https://api-staging.instat.tv'
+export const API_ROOT = 'https://api.instat.tv'
export const DATA_URL = `${API_ROOT}/data`
diff --git a/src/features/App/RedirectCallback.tsx b/src/features/App/RedirectCallback.tsx
new file mode 100644
index 00000000..e7a62103
--- /dev/null
+++ b/src/features/App/RedirectCallback.tsx
@@ -0,0 +1,16 @@
+import { useEffect } from 'react'
+
+import { useAuthStore } from 'features/AuthStore'
+import { queryParamStorage } from 'features/QueryParamsStorage'
+
+export const RedirectCallback = () => {
+ const { signinRedirectCallback } = useAuthStore()
+
+ useEffect(() => {
+ signinRedirectCallback().then(() => {
+ queryParamStorage.clear()
+ })
+ }, [signinRedirectCallback])
+
+ return null
+}
diff --git a/src/features/App/UnauthenticatedApp.tsx b/src/features/App/UnauthenticatedApp.tsx
index e8d30f04..7fd733cc 100644
--- a/src/features/App/UnauthenticatedApp.tsx
+++ b/src/features/App/UnauthenticatedApp.tsx
@@ -1,8 +1,8 @@
import { lazy } from 'react'
import {
- Route,
Redirect,
+ Route,
Switch,
} from 'react-router-dom'
@@ -15,6 +15,8 @@ import { useLexicsConfig } from 'features/LexicsStore'
import { LanguageSelect } from 'features/LanguageSelect'
import { HeaderStyled, HeaderGroup } from 'features/ProfileHeader/styled'
+import { RedirectCallback } from './RedirectCallback'
+
const Login = lazy(() => import('features/Login'))
const Register = lazy(() => import('features/Register'))
@@ -42,6 +44,10 @@ export const UnauthenticatedApp = () => {
+
+
+
+
diff --git a/src/features/App/index.tsx b/src/features/App/index.tsx
index 7cbf2d91..0b107907 100644
--- a/src/features/App/index.tsx
+++ b/src/features/App/index.tsx
@@ -13,11 +13,16 @@ import { AuthenticatedApp } from './AuthenticatedApp'
import { UnauthenticatedApp } from './UnauthenticatedApp'
const Main = () => {
- const { token } = useAuthStore()
+ const { loadingUser, user } = useAuthStore()
- return token
- ?
- :
+ // юзер считывается из localstorage или
+ // access_token токен истек и запрашивается новый
+ if (loadingUser || user?.expired) return null
+
+ if (!user) return
+
+ // имеется действующий токен
+ return
}
export const App = () => (
diff --git a/src/features/AuthStore/helpers.tsx b/src/features/AuthStore/helpers.tsx
new file mode 100644
index 00000000..aa9958ca
--- /dev/null
+++ b/src/features/AuthStore/helpers.tsx
@@ -0,0 +1,19 @@
+import type { UserManagerSettings } from 'oidc-client'
+import { WebStorageStateStore } from 'oidc-client'
+
+export const getClientSettings = (): UserManagerSettings => ({
+ authority: 'https://api.instat.tv/',
+ automaticSilentRenew: true,
+ client_id: 'ott-web',
+ filterProtocolClaims: false,
+ loadUserInfo: false,
+ metadataUrl: 'https://api.instat.tv/auth/.well-known/openid-configuration',
+ post_logout_redirect_uri: `${window.origin}/login`,
+ redirect_uri: `${window.origin}/redirect`,
+ response_mode: 'query',
+ response_type: 'id_token token',
+ scope: 'openid',
+ silent_redirect_uri: `${window.origin}/silent-refresh.html`,
+ silentRequestTimeout: 5000,
+ userStore: new WebStorageStateStore({ store: window.localStorage }),
+})
diff --git a/src/features/AuthStore/hooks/useAuth.tsx b/src/features/AuthStore/hooks/useAuth.tsx
new file mode 100644
index 00000000..f7f2881e
--- /dev/null
+++ b/src/features/AuthStore/hooks/useAuth.tsx
@@ -0,0 +1,95 @@
+import {
+ useCallback,
+ useState,
+ useMemo,
+ useEffect,
+} from 'react'
+
+import type { User } from 'oidc-client'
+import { UserManager } from 'oidc-client'
+
+import { writeToken, removeToken } from 'helpers/token'
+
+import { useToggle } from 'hooks'
+
+import { getClientSettings } from '../helpers'
+import { useRegister } from './useRegister'
+
+export const useAuth = () => {
+ const {
+ close: markUserLoaded,
+ isOpen: loadingUser,
+ } = useToggle(true)
+ const [user, setUser] = useState()
+ const userManager = useMemo(() => new UserManager(getClientSettings()), [])
+
+ const login = useCallback(async () => (
+ userManager.signinRedirect()
+ ), [userManager])
+
+ const logout = useCallback(async () => {
+ userManager.signoutRedirect()
+ userManager.clearStaleState()
+ removeToken()
+ }, [userManager])
+
+ const register = useRegister()
+
+ const storeUser = useCallback(async (loadedUser: User) => {
+ setUser(loadedUser)
+ writeToken(loadedUser.access_token)
+ }, [])
+
+ const checkUser = useCallback(async () => {
+ const loadedUser = await userManager.getUser()
+ if (loadedUser) {
+ storeUser(loadedUser)
+ }
+ markUserLoaded()
+ }, [userManager, storeUser, markUserLoaded])
+
+ useEffect(() => {
+ // при первой загрузке считываем пользователя из localstorage
+ checkUser()
+ }, [checkUser])
+
+ useEffect(() => {
+ // попытаемся обновить токен используя refresh_token
+ const tryRenewToken = () => {
+ userManager.signinSilent().catch(logout)
+ }
+ // если запросы вернули 401 | 403
+ window.addEventListener('FORBIDDEN_REQUEST', tryRenewToken)
+
+ // и если токен истек
+ userManager.events.addAccessTokenExpired(tryRenewToken)
+ return () => {
+ window.removeEventListener('FORBIDDEN_REQUEST', tryRenewToken)
+ userManager.events.removeAccessTokenExpired(tryRenewToken)
+ }
+ }, [userManager, logout])
+
+ useEffect(() => {
+ // событие срабатывает после получения токена(первый логин и обновление токена)
+ userManager.events.addUserLoaded(storeUser)
+ return () => userManager.events.removeUserLoaded(storeUser)
+ }, [userManager, storeUser])
+
+ const auth = useMemo(() => ({
+ loadingUser,
+ login,
+ logout,
+ register,
+ signinRedirectCallback: () => userManager.signinRedirectCallback(),
+ user,
+ }), [
+ login,
+ logout,
+ register,
+ user,
+ loadingUser,
+ userManager,
+ ])
+
+ return auth
+}
diff --git a/src/features/AuthStore/hooks/useLogin.tsx b/src/features/AuthStore/hooks/useLogin.tsx
deleted file mode 100644
index 68ec11c3..00000000
--- a/src/features/AuthStore/hooks/useLogin.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import { useHistory } from 'react-router-dom'
-
-import { PAGES } from 'config'
-import { login } from 'requests'
-import { writeToken } from 'helpers'
-
-type LoginArgs = Parameters[0]
-
-type Args = {
- setToken: (token: string | null) => void,
-}
-
-export const useLogin = ({ setToken }: Args) => {
- const history = useHistory()
-
- const onSuccess = (token: string) => {
- writeToken(token)
- setToken(token)
- history.replace(PAGES.home)
- }
-
- return async ({ email, password }: LoginArgs) => (
- login({ email, password }).then(onSuccess)
- )
-}
diff --git a/src/features/AuthStore/hooks/useLogout.tsx b/src/features/AuthStore/hooks/useLogout.tsx
deleted file mode 100644
index 1658e169..00000000
--- a/src/features/AuthStore/hooks/useLogout.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import { useHistory } from 'react-router-dom'
-
-import { PAGES } from 'config'
-import { removeToken } from 'helpers'
-import { logout } from 'requests'
-
-type Args = {
- setToken: (token: string | null) => void,
-}
-
-export const useLogout = ({ setToken }: Args) => {
- const history = useHistory()
-
- return () => {
- logout()
- removeToken()
- setToken(null)
- history.replace(PAGES.login)
- }
-}
diff --git a/src/features/AuthStore/index.tsx b/src/features/AuthStore/index.tsx
index e51504a5..78262074 100644
--- a/src/features/AuthStore/index.tsx
+++ b/src/features/AuthStore/index.tsx
@@ -2,21 +2,11 @@ import type { ReactNode } from 'react'
import {
createContext,
useContext,
- useState,
} from 'react'
-import { readToken } from 'helpers'
+import { useAuth } from './hooks/useAuth'
-import { useLogin } from './hooks/useLogin'
-import { useLogout } from './hooks/useLogout'
-import { useRegister } from './hooks/useRegister'
-
-type Auth = {
- login: ReturnType,
- logout: ReturnType,
- register: ReturnType,
- token: string | null,
-}
+type Auth = ReturnType
const AuthContext = createContext({} as Auth)
@@ -25,17 +15,7 @@ type Props = {
}
export const AuthStore = ({ children }: Props) => {
- const [token, setToken] = useState(readToken())
- const login = useLogin({ setToken })
- const logout = useLogout({ setToken })
- const register = useRegister()
-
- const auth = {
- login,
- logout,
- register,
- token,
- }
+ const auth = useAuth()
return {children}
}
diff --git a/src/features/Login/hooks.tsx b/src/features/Login/hooks.tsx
index 064532b7..35ac2541 100644
--- a/src/features/Login/hooks.tsx
+++ b/src/features/Login/hooks.tsx
@@ -1,71 +1,19 @@
-import type { FormEvent, FocusEvent } from 'react'
-
-import trim from 'lodash/trim'
-import isEmpty from 'lodash/isEmpty'
-
-import { formIds } from 'config/form'
-
-import { isValidEmail } from 'helpers/isValidEmail'
+import type { FormEvent } from 'react'
import { useAuthStore } from 'features/AuthStore'
import { useForm } from 'features/FormStore'
export const useLoginForm = () => {
- const {
- readFormError,
- readFormValue,
- updateFormError,
- updateFormValue,
- } = useForm()
+ const { readFormError } = useForm()
const { login } = useAuthStore()
- const onEmailBlur = ({ target }: FocusEvent) => {
- const email = target.value
- if (email && !isValidEmail(email)) {
- updateFormError(formIds.email, 'error_invalid_email_format')
- }
- }
-
- const readTrimmedValue = (fieldName: string) => trim(readFormValue(fieldName))
-
- const validateForm = () => {
- let hasError = false
- const email = readTrimmedValue(formIds.email)
- const password = readTrimmedValue(formIds.password)
- if (isEmpty(email)) {
- updateFormError(formIds.email, 'error_empty_email')
- hasError = true
- } else if (!isValidEmail(email)) {
- updateFormError(formIds.email, 'error_invalid_email_format')
- hasError = true
- }
- if (isEmpty(password)) {
- updateFormError(formIds.password, 'error_empty_password')
- hasError = true
- }
- return !hasError
- }
-
- const onError = (message: string) => {
- updateFormError(formIds.formError, message)
- }
-
const handleSubmit = async (event: FormEvent) => {
event.preventDefault()
-
- if (validateForm()) {
- const email = readTrimmedValue(formIds.email)
- const password = readTrimmedValue(formIds.password)
-
- login({ email, password }).catch(onError)
- }
+ login()
}
return {
handleSubmit,
- onEmailBlur,
readFormError,
- readFormValue,
- updateFormValue,
}
}
diff --git a/src/features/Login/index.tsx b/src/features/Login/index.tsx
index e1ddcb0b..9f85e47b 100644
--- a/src/features/Login/index.tsx
+++ b/src/features/Login/index.tsx
@@ -3,7 +3,7 @@ import { formIds } from 'config/form'
import { T9n } from 'features/T9n'
import { Logo } from 'features/Logo'
-import { Input, ButtonSolid } from 'features/Common'
+import { ButtonSolid } from 'features/Common'
import { Error } from 'features/Common/Input/styled'
import { FormStore } from 'features/FormStore'
@@ -16,15 +16,10 @@ import {
RegisterButton,
} from './styled'
-const labelWidth = 75
-
const LoginForm = () => {
const {
handleSubmit,
- onEmailBlur,
readFormError,
- readFormValue,
- updateFormValue,
} = useLoginForm()
const requestError = readFormError(formIds.formError)
@@ -35,24 +30,6 @@ const LoginForm = () => {