fix: authorization / registration
This commit is contained in:
@@ -12,9 +12,14 @@ export interface RegistrationCompletePayload {
|
||||
code: string
|
||||
}
|
||||
|
||||
export interface LoginPayload {
|
||||
export interface LoginStartPayload {
|
||||
email: string
|
||||
}
|
||||
|
||||
export interface LoginCompletePayload {
|
||||
email: string
|
||||
password: string
|
||||
code: string
|
||||
}
|
||||
|
||||
export interface AuthResponse {
|
||||
@@ -29,8 +34,12 @@ export function registrationComplete(payload: RegistrationCompletePayload): Prom
|
||||
return api.post<AuthResponse>('/auth/registration/complete', payload)
|
||||
}
|
||||
|
||||
export function login(payload: LoginPayload): Promise<AuthResponse> {
|
||||
return api.post<AuthResponse>('/auth/login', payload)
|
||||
export function loginStart(payload: LoginStartPayload): Promise<void> {
|
||||
return api.post<void>('/auth/login/start', payload)
|
||||
}
|
||||
|
||||
export function loginComplete(payload: LoginCompletePayload): Promise<AuthResponse> {
|
||||
return api.post<AuthResponse>('/auth/login/complete', payload)
|
||||
}
|
||||
|
||||
export async function logout(): Promise<void> {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export { registrationStart, registrationComplete, login } from './api/registrationApi'
|
||||
export type { RegistrationStartPayload, RegistrationCompletePayload, LoginPayload, AuthResponse } from './api/registrationApi'
|
||||
export { registrationStart, registrationComplete, loginStart, loginComplete } from './api/registrationApi'
|
||||
export type { RegistrationStartPayload, RegistrationCompletePayload, LoginStartPayload, LoginCompletePayload, AuthResponse } from './api/registrationApi'
|
||||
export { useIsAuthenticated } from './hooks/useIsAuthenticated'
|
||||
export { useAuth, AUTH_QUERY_KEY } from './hooks/useAuth'
|
||||
|
||||
@@ -1,21 +1,33 @@
|
||||
import { useState } from 'react'
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { useNavigate, useLocation } from 'react-router-dom'
|
||||
import { login } from '@features/auth'
|
||||
import { loginStart, loginComplete, AUTH_QUERY_KEY } from '@features/auth'
|
||||
import { tokenStore } from '@shared/api/tokenStore'
|
||||
import { AUTH_QUERY_KEY } from '@features/auth'
|
||||
import { ROUTES } from '@shared/config/routes'
|
||||
import type { ApiErrorResponse } from '@shared/api/types'
|
||||
|
||||
function extractErrorMessage(error: unknown): string {
|
||||
const apiError = error as ApiErrorResponse
|
||||
return apiError?.detail?.[0]?.msg ?? 'Произошла ошибка'
|
||||
}
|
||||
|
||||
export function useLoginForm() {
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [verificationCode, setVerificationCode] = useState('')
|
||||
const [codeSent, setCodeSent] = useState(false)
|
||||
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,
|
||||
const startMutation = useMutation({
|
||||
mutationFn: loginStart,
|
||||
onSuccess: () => setCodeSent(true),
|
||||
})
|
||||
|
||||
const completeMutation = useMutation({
|
||||
mutationFn: loginComplete,
|
||||
onSuccess: ({ access_token }) => {
|
||||
tokenStore.set(access_token)
|
||||
queryClient.setQueryData(AUTH_QUERY_KEY, access_token)
|
||||
@@ -23,14 +35,29 @@ export function useLoginForm() {
|
||||
},
|
||||
})
|
||||
|
||||
const handleRequestCode = () => {
|
||||
if (!email) return
|
||||
startMutation.mutate({ email })
|
||||
}
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
completeMutation.mutate({ email, password, code: verificationCode })
|
||||
}
|
||||
|
||||
const error =
|
||||
(startMutation.isError ? extractErrorMessage(startMutation.error) : null) ??
|
||||
(completeMutation.isError ? extractErrorMessage(completeMutation.error) : null)
|
||||
|
||||
return {
|
||||
email, setEmail,
|
||||
password, setPassword,
|
||||
isLoading: mutation.isPending,
|
||||
error: mutation.isError ? 'Неверный email или пароль' : null,
|
||||
handleSubmit: (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
mutation.mutate({ email, password })
|
||||
},
|
||||
verificationCode, setVerificationCode,
|
||||
codeSent,
|
||||
isLoadingCode: startMutation.isPending,
|
||||
isLoadingSubmit: completeMutation.isPending,
|
||||
error,
|
||||
handleRequestCode,
|
||||
handleSubmit,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
background: var(--glass-bg);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: 24px;
|
||||
padding: 48px 40px;
|
||||
padding: 32px;
|
||||
width: 100%;
|
||||
max-width: 480px;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
@@ -26,10 +26,48 @@
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.fields {
|
||||
.twoCol {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 20px 24px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.leftCol {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.rightCol {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.codeHint {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #ff5a5a;
|
||||
font-size: 13px;
|
||||
margin-top: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.submitWrapper {
|
||||
margin-top: 28px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.forgot {
|
||||
@@ -37,7 +75,7 @@
|
||||
text-align: right;
|
||||
font-size: 13px;
|
||||
color: var(--interactive);
|
||||
margin: 8px 0 16px;
|
||||
margin-bottom: 16px;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition: color 0.2s;
|
||||
@@ -47,17 +85,11 @@
|
||||
color: var(--highlight);
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin: 24px 0;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.divider::before,
|
||||
@@ -73,14 +105,13 @@
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
@media (max-width: 520px) {
|
||||
@media (max-width: 560px) {
|
||||
.card {
|
||||
padding: 32px 20px;
|
||||
min-height: 100vh;
|
||||
min-width: 100%;
|
||||
border-radius: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.twoCol {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,17 @@ import styles from './LoginForm.module.css'
|
||||
import { useLoginForm } from '../model/useLoginForm'
|
||||
|
||||
export function LoginForm() {
|
||||
const { email, setEmail, password, setPassword, isLoading, error, handleSubmit } = useLoginForm()
|
||||
const {
|
||||
email, setEmail,
|
||||
password, setPassword,
|
||||
verificationCode, setVerificationCode,
|
||||
codeSent,
|
||||
isLoadingCode,
|
||||
isLoadingSubmit,
|
||||
error,
|
||||
handleRequestCode,
|
||||
handleSubmit,
|
||||
} = useLoginForm()
|
||||
const navigate = useNavigate()
|
||||
|
||||
return (
|
||||
@@ -18,7 +28,8 @@ export function LoginForm() {
|
||||
</div>
|
||||
<h1 className={styles.title}>Войти в кошелёк ЭКСА</h1>
|
||||
|
||||
<div className={styles.fields}>
|
||||
<div className={styles.twoCol}>
|
||||
<div className={styles.leftCol}>
|
||||
<FormField
|
||||
label="Адрес электронной почты"
|
||||
type="email"
|
||||
@@ -37,14 +48,37 @@ export function LoginForm() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.rightCol}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
type="button"
|
||||
onClick={handleRequestCode}
|
||||
disabled={codeSent || isLoadingCode}
|
||||
>
|
||||
{isLoadingCode ? 'Отправка...' : codeSent ? 'Код отправлен' : 'Получить код на email'}
|
||||
</Button>
|
||||
<span className={styles.codeHint}>Код не пришёл</span>
|
||||
<FormField
|
||||
label="Ввести код"
|
||||
type="text"
|
||||
value={verificationCode}
|
||||
onChange={setVerificationCode}
|
||||
placeholder="000 000"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && <p className={styles.error}>{error}</p>}
|
||||
|
||||
<a className={styles.forgot}>Забыли пароль?</a>
|
||||
<div className={styles.submitWrapper}>
|
||||
<PrimaryButton label={isLoadingSubmit ? 'Вход...' : 'Войти'} disabled={isLoadingSubmit} />
|
||||
</div>
|
||||
|
||||
<div className={styles.actions}>
|
||||
<PrimaryButton label="Войти" disabled={isLoading} />
|
||||
<div className={styles.footer}>
|
||||
<a className={styles.forgot}>Забыли пароль?</a>
|
||||
<div className={styles.divider}><span>или</span></div>
|
||||
<Button variant="outline" onClick={() => navigate(ROUTES.REGISTER)}>
|
||||
<Button variant="outline" type="button" onClick={() => navigate(ROUTES.REGISTER)}>
|
||||
Создать новый кошелёк
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user