base commit
This commit is contained in:
@@ -0,0 +1 @@
|
||||
export { AuthPage } from './ui/AuthPage'
|
||||
@@ -0,0 +1,139 @@
|
||||
import { useState } from 'react'
|
||||
import Alert from '@mui/material/Alert'
|
||||
import Box from '@mui/material/Box'
|
||||
import Button from '@mui/material/Button'
|
||||
import Divider from '@mui/material/Divider'
|
||||
import Stack from '@mui/material/Stack'
|
||||
import TextField from '@mui/material/TextField'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { apiClient } from '@/shared/api/client'
|
||||
import { tokenSet } from '@/shared/model/auth'
|
||||
|
||||
type AuthResponse = { token: string; user: { id: string; email: string } }
|
||||
|
||||
function getApiErrorMessage(err: unknown): string | null {
|
||||
if (!err || typeof err !== 'object') return null
|
||||
const anyErr = err as Record<string, unknown>
|
||||
const response = anyErr.response as Record<string, unknown> | undefined
|
||||
const data = response?.data as Record<string, unknown> | undefined
|
||||
const msg = data?.error
|
||||
return typeof msg === 'string' ? msg : null
|
||||
}
|
||||
|
||||
export function AuthPage() {
|
||||
const [message, setMessage] = useState<string | null>(null)
|
||||
const { register, watch } = useForm<{
|
||||
email: string
|
||||
code: string
|
||||
password: string
|
||||
}>({
|
||||
defaultValues: { email: '', code: '', password: '' },
|
||||
mode: 'onChange',
|
||||
})
|
||||
|
||||
const email = watch('email')
|
||||
const code = watch('code')
|
||||
const password = watch('password')
|
||||
|
||||
const requestCode = useMutation({
|
||||
mutationFn: async () => {
|
||||
await apiClient.post('auth/request-code', { email })
|
||||
},
|
||||
onSuccess: () => setMessage('Код отправлен. Проверьте почту (в dev может быть в логах сервера).'),
|
||||
})
|
||||
|
||||
const verifyCode = useMutation({
|
||||
mutationFn: async () => {
|
||||
const { data } = await apiClient.post<AuthResponse>('auth/verify-code', { email, code })
|
||||
tokenSet(data.token)
|
||||
setMessage(`Вход выполнен: ${data.user.email}`)
|
||||
},
|
||||
})
|
||||
|
||||
const registerPassword = useMutation({
|
||||
mutationFn: async () => {
|
||||
const { data } = await apiClient.post<AuthResponse>('auth/register', { email, password })
|
||||
tokenSet(data.token)
|
||||
setMessage(`Регистрация выполнена: ${data.user.email}`)
|
||||
},
|
||||
})
|
||||
|
||||
const loginPassword = useMutation({
|
||||
mutationFn: async () => {
|
||||
const { data } = await apiClient.post<AuthResponse>('auth/login', { email, password })
|
||||
tokenSet(data.token)
|
||||
setMessage(`Вход выполнен: ${data.user.email}`)
|
||||
},
|
||||
})
|
||||
|
||||
const errMsg = getApiErrorMessage(
|
||||
requestCode.error || verifyCode.error || registerPassword.error || loginPassword.error,
|
||||
)
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Typography variant="h4" gutterBottom>
|
||||
Вход / регистрация
|
||||
</Typography>
|
||||
|
||||
{message && (
|
||||
<Alert severity="success" sx={{ mb: 2 }}>
|
||||
{message}
|
||||
</Alert>
|
||||
)}
|
||||
{errMsg && (
|
||||
<Alert severity="error" sx={{ mb: 2 }}>
|
||||
{errMsg}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Stack spacing={2} sx={{ maxWidth: 520 }}>
|
||||
<TextField label="Email" {...register('email')} fullWidth />
|
||||
|
||||
<Typography variant="h6">Вариант 1: Email + код</Typography>
|
||||
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2}>
|
||||
<Button variant="outlined" onClick={() => requestCode.mutate()} disabled={!email || requestCode.isPending}>
|
||||
Отправить код
|
||||
</Button>
|
||||
<TextField label="Код (6 цифр)" inputMode="numeric" {...register('code')} />
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => verifyCode.mutate()}
|
||||
disabled={!email || code.length !== 6 || verifyCode.isPending}
|
||||
>
|
||||
Войти
|
||||
</Button>
|
||||
</Stack>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Typography variant="h6">Вариант 2: Email + пароль</Typography>
|
||||
<TextField
|
||||
label="Пароль"
|
||||
type="password"
|
||||
{...register('password')}
|
||||
fullWidth
|
||||
helperText="Минимум 8 символов для регистрации"
|
||||
/>
|
||||
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2}>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => registerPassword.mutate()}
|
||||
disabled={!email || password.length < 8 || registerPassword.isPending}
|
||||
>
|
||||
Зарегистрироваться
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={() => loginPassword.mutate()}
|
||||
disabled={!email || !password || loginPassword.isPending}
|
||||
>
|
||||
Войти
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user