diff --git a/src/app/providers/ProtectedRoute.tsx b/src/app/providers/ProtectedRoute.tsx
index f3e020b..595b2d0 100644
--- a/src/app/providers/ProtectedRoute.tsx
+++ b/src/app/providers/ProtectedRoute.tsx
@@ -3,12 +3,10 @@ import { useIsAuthenticated } from '@features/auth'
import { ROUTES } from '@shared/config/routes'
export function ProtectedRoute() {
- const isAuthenticated = useIsAuthenticated()
+ const { isAuthenticated, isLoading } = useIsAuthenticated()
const location = useLocation()
- if (!isAuthenticated) {
- return
- }
-
+ if (isLoading) return null
+ if (!isAuthenticated) return
return
}
diff --git a/src/features/auth/api/registrationApi.ts b/src/features/auth/api/registrationApi.ts
index 160187c..b1b7811 100644
--- a/src/features/auth/api/registrationApi.ts
+++ b/src/features/auth/api/registrationApi.ts
@@ -12,12 +12,25 @@ export interface RegistrationCompletePayload {
code: string
}
-export function registrationStart(payload: RegistrationStartPayload): Promise {
- return api.post('/auth/registration/start', payload)
+export interface LoginPayload {
+ email: string
+ password: string
}
-export function registrationComplete(payload: RegistrationCompletePayload): Promise {
- return api.post('/auth/registration/complete', payload)
+export interface AuthResponse {
+ access_token: string
+}
+
+export function registrationStart(payload: RegistrationStartPayload): Promise {
+ return api.post('/auth/registration/start', payload)
+}
+
+export function registrationComplete(payload: RegistrationCompletePayload): Promise {
+ return api.post('/auth/registration/complete', payload)
+}
+
+export function login(payload: LoginPayload): Promise {
+ return api.post('/auth/login', payload)
}
export async function logout(): Promise {
diff --git a/src/features/auth/hooks/useAuth.ts b/src/features/auth/hooks/useAuth.ts
new file mode 100644
index 0000000..07a7430
--- /dev/null
+++ b/src/features/auth/hooks/useAuth.ts
@@ -0,0 +1,13 @@
+import { useQuery } from '@tanstack/react-query'
+import { refreshAccessToken } from '@shared/api/tokenStore'
+
+export const AUTH_QUERY_KEY = ['auth']
+
+export function useAuth() {
+ return useQuery({
+ queryKey: AUTH_QUERY_KEY,
+ queryFn: refreshAccessToken,
+ retry: false,
+ staleTime: Infinity,
+ })
+}
diff --git a/src/features/auth/hooks/useIsAuthenticated.ts b/src/features/auth/hooks/useIsAuthenticated.ts
index 0b58d38..c1caa44 100644
--- a/src/features/auth/hooks/useIsAuthenticated.ts
+++ b/src/features/auth/hooks/useIsAuthenticated.ts
@@ -1,3 +1,6 @@
-export function useIsAuthenticated(): boolean {
- return localStorage.getItem('isAuthenticated') === 'true'
+import { useAuth } from './useAuth'
+
+export function useIsAuthenticated(): { isAuthenticated: boolean; isLoading: boolean } {
+ const { data, isLoading, isError } = useAuth()
+ return { isAuthenticated: !!data && !isError, isLoading }
}
diff --git a/src/features/auth/index.ts b/src/features/auth/index.ts
index d7b647f..444233b 100644
--- a/src/features/auth/index.ts
+++ b/src/features/auth/index.ts
@@ -1,3 +1,4 @@
-export { registrationStart, registrationComplete } from './api/registrationApi'
-export type { RegistrationStartPayload, RegistrationCompletePayload } from './api/registrationApi'
+export { registrationStart, registrationComplete, login } from './api/registrationApi'
+export type { RegistrationStartPayload, RegistrationCompletePayload, LoginPayload, AuthResponse } from './api/registrationApi'
export { useIsAuthenticated } from './hooks/useIsAuthenticated'
+export { useAuth, AUTH_QUERY_KEY } from './hooks/useAuth'
diff --git a/src/shared/api/base.ts b/src/shared/api/base.ts
index dff3e34..78f0b9d 100644
--- a/src/shared/api/base.ts
+++ b/src/shared/api/base.ts
@@ -1,28 +1,37 @@
import { API_URL } from '@shared/config/env'
import { getCsrfToken } from './csrf'
+import { tokenStore, refreshAccessToken } from './tokenStore'
-async function request(path: string, options: RequestInit = {}): Promise {
- const token = await getCsrfToken()
+async function doRequest(path: string, options: RequestInit, allowRetry: boolean): Promise {
+ const csrf = await getCsrfToken()
+ const bearer = tokenStore.get()
- const res = await fetch(`${API_URL}${path}`, {
- ...options,
- credentials: 'include',
- headers: {
- 'Content-Type': 'application/json',
- 'X-CSRF-Token': token,
- ...options.headers,
- },
- })
+ const headers: HeadersInit = {
+ 'Content-Type': 'application/json',
+ 'X-CSRF-Token': csrf,
+ ...(bearer ? { Authorization: `Bearer ${bearer}` } : {}),
+ ...options.headers,
+ }
+
+ const res = await fetch(`${API_URL}${path}`, { ...options, credentials: 'include', headers })
+
+ if (res.status === 401 && allowRetry) {
+ try {
+ await refreshAccessToken()
+ return doRequest(path, options, false)
+ } catch {
+ tokenStore.clear()
+ throw new Error('Unauthorized')
+ }
+ }
const data = await res.json()
-
if (!res.ok) throw data
-
return data as T
}
export const api = {
- get: (path: string) => request(path),
+ get: (path: string) => doRequest(path, {}, true),
post: (path: string, body: unknown) =>
- request(path, { method: 'POST', body: JSON.stringify(body) }),
+ doRequest(path, { method: 'POST', body: JSON.stringify(body) }, true),
}
diff --git a/src/shared/api/tokenStore.ts b/src/shared/api/tokenStore.ts
new file mode 100644
index 0000000..5ceec58
--- /dev/null
+++ b/src/shared/api/tokenStore.ts
@@ -0,0 +1,17 @@
+let accessToken: string | null = null
+
+export const tokenStore = {
+ get: () => accessToken,
+ set: (token: string) => { accessToken = token },
+ clear: () => { accessToken = null },
+}
+
+const REFRESH_URL = 'https://app.auth.elcsa.ru/v1/jwt/refresh'
+
+export async function refreshAccessToken(): Promise {
+ const res = await fetch(REFRESH_URL, { method: 'POST', credentials: 'include' })
+ if (!res.ok) throw new Error('Unauthorized')
+ const data = await res.json()
+ tokenStore.set(data.access_token)
+ return data.access_token as string
+}
diff --git a/src/widgets/login-form/model/useLoginForm.ts b/src/widgets/login-form/model/useLoginForm.ts
new file mode 100644
index 0000000..e5b770c
--- /dev/null
+++ b/src/widgets/login-form/model/useLoginForm.ts
@@ -0,0 +1,36 @@
+import { useState } from 'react'
+import { useMutation, useQueryClient } from '@tanstack/react-query'
+import { useNavigate, useLocation } from 'react-router-dom'
+import { login } from '@features/auth'
+import { tokenStore } from '@shared/api/tokenStore'
+import { AUTH_QUERY_KEY } from '@features/auth'
+import { ROUTES } from '@shared/config/routes'
+
+export function useLoginForm() {
+ const [email, setEmail] = useState('')
+ const [password, setPassword] = useState('')
+ const navigate = useNavigate()
+ const location = useLocation()
+ const queryClient = useQueryClient()
+ const from = (location.state as { from?: { pathname: string } })?.from?.pathname ?? ROUTES.WALLET
+
+ const mutation = useMutation({
+ mutationFn: login,
+ onSuccess: ({ access_token }) => {
+ tokenStore.set(access_token)
+ queryClient.setQueryData(AUTH_QUERY_KEY, access_token)
+ navigate(from, { replace: true })
+ },
+ })
+
+ return {
+ email, setEmail,
+ password, setPassword,
+ isLoading: mutation.isPending,
+ error: mutation.isError ? 'Неверный email или пароль' : null,
+ handleSubmit: (e: React.FormEvent) => {
+ e.preventDefault()
+ mutation.mutate({ email, password })
+ },
+ }
+}
diff --git a/src/widgets/login-form/ui/LoginForm.tsx b/src/widgets/login-form/ui/LoginForm.tsx
index a0c3f2f..1b1bf10 100644
--- a/src/widgets/login-form/ui/LoginForm.tsx
+++ b/src/widgets/login-form/ui/LoginForm.tsx
@@ -1,4 +1,3 @@
-import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { FormField } from '@shared/ui'
import { PrimaryButton } from '@shared/ui'
@@ -6,14 +5,14 @@ import { Button } from '@shared/ui'
import { ROUTES } from '@shared/config/routes'
import logo from '@shared/assets/logo-full-white.png'
import styles from './LoginForm.module.css'
+import { useLoginForm } from '../model/useLoginForm'
export function LoginForm() {
- const [email, setEmail] = useState('')
- const [password, setPassword] = useState('')
+ const { email, setEmail, password, setPassword, isLoading, error, handleSubmit } = useLoginForm()
const navigate = useNavigate()
return (
-