From 8b0e787fc6fa1446ba69c9954ab779234404dd44 Mon Sep 17 00:00:00 2001 From: rassadin11 Date: Sun, 10 May 2026 19:24:32 +0300 Subject: [PATCH] fix: authorization / registration --- src/app/providers/ProtectedRoute.tsx | 8 ++-- src/features/auth/api/registrationApi.ts | 21 ++++++++-- src/features/auth/hooks/useAuth.ts | 13 +++++++ src/features/auth/hooks/useIsAuthenticated.ts | 7 +++- src/features/auth/index.ts | 5 ++- src/shared/api/base.ts | 39 ++++++++++++------- src/shared/api/tokenStore.ts | 17 ++++++++ src/widgets/login-form/model/useLoginForm.ts | 36 +++++++++++++++++ src/widgets/login-form/ui/LoginForm.tsx | 11 +++--- .../register-form/model/useRegisterForm.ts | 11 ++++-- src/widgets/wallet-header/ui/WalletHeader.tsx | 11 +++++- 11 files changed, 140 insertions(+), 39 deletions(-) create mode 100644 src/features/auth/hooks/useAuth.ts create mode 100644 src/shared/api/tokenStore.ts create mode 100644 src/widgets/login-form/model/useLoginForm.ts 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 ( -
+
ЭКСА
@@ -38,10 +37,12 @@ export function LoginForm() { /> + {error &&

{error}

} + Забыли пароль?
- +
или