base commit

This commit is contained in:
@kirill.komarov
2026-04-28 21:36:30 +05:00
parent 55480d4aa5
commit 2148fd7a12
24 changed files with 1578 additions and 121 deletions
+139
View File
@@ -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>
)
}