Files
shop-server/client/src/pages/me/ui/sections/AuthMethodsSection.tsx
T
Kirill be9a9bad8e fix(Task 2): add error handling and sync state with user
- AuthMethodsSection: show Alert on fetchAuthMethodsFx failure
- AvatarSection: sync selectedStyle with user.avatarStyle via Select key
- ProfileSection: reset form defaultValues when user.displayName changes
2026-05-22 15:15:50 +05:00

219 lines
8.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useCallback, useEffect, useState } from 'react'
import Alert from '@mui/material/Alert'
import Box from '@mui/material/Box'
import Button from '@mui/material/Button'
import Chip from '@mui/material/Chip'
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 { useUnit } from 'effector-react'
import { useForm } from 'react-hook-form'
import {
$user,
changePasswordFx,
fetchAuthMethodsFx,
setPasswordFx,
unlinkOAuthFx,
type AuthMethod,
} from '@/shared/model/auth'
const METHOD_LABELS: Record<string, string> = { password: 'Пароль', vk: 'ВКонтакте', yandex: 'Яндекс' }
export function AuthMethodsSection() {
const user = useUnit($user)
const [authMethods, setAuthMethods] = useState<AuthMethod[]>([])
const [showSetPassword, setShowSetPassword] = useState(false)
const [fetchError, setFetchError] = useState<string | null>(null)
const passwordForm = useForm<{ password: string; passwordConfirm: string }>({
defaultValues: { password: '', passwordConfirm: '' },
})
useEffect(() => {
fetchAuthMethodsFx()
.then(setAuthMethods)
.catch((err) => {
setAuthMethods([])
setFetchError(err?.message || 'Не удалось загрузить методы авторизации')
})
}, [])
const setPasswordMutation = useMutation({
mutationFn: async (pw: string) => {
await setPasswordFx(pw)
const methods = await fetchAuthMethodsFx()
setAuthMethods(methods)
setShowSetPassword(false)
},
onError: () => {},
})
const unlinkMutation = useMutation({
mutationFn: async (provider: 'vk' | 'yandex') => {
await unlinkOAuthFx(provider)
const methods = await fetchAuthMethodsFx()
setAuthMethods(methods)
},
onError: () => {},
})
const [showChangePassword, setShowChangePassword] = useState(false)
const changePasswordForm = useForm<{ oldPassword: string; newPassword: string; confirmPassword: string }>({
defaultValues: { oldPassword: '', newPassword: '', confirmPassword: '' },
})
const changePasswordMutation = useMutation({
mutationFn: async (params: { oldPassword: string; newPassword: string }) => {
await changePasswordFx(params)
},
onSuccess: () => {
setShowChangePassword(false)
changePasswordForm.reset()
},
})
const linkedCount = useCallback(() => {
return authMethods.filter((m) => m.active).length
}, [authMethods])
if (!user) return null
return (
<Box>
<Typography variant="h6" gutterBottom>
Методы входа
</Typography>
{fetchError && (
<Alert severity="error" sx={{ mb: 2 }}>
{fetchError}
</Alert>
)}
<Stack spacing={1}>
{authMethods.map((m) => (
<Stack key={m.type} direction="row" spacing={2} sx={{ alignItems: 'center' }}>
<Typography sx={{ minWidth: 120 }}>{METHOD_LABELS[m.type] || m.type}</Typography>
<Chip label={m.active ? 'Привязан' : 'Не привязан'} color={m.active ? 'success' : 'default'} size="small" />
{m.active && m.type !== 'password' && (
<Button
size="small"
variant="outlined"
color="error"
disabled={linkedCount() <= 1}
onClick={() => unlinkMutation.mutate(m.type as 'vk' | 'yandex')}
>
Отвязать
</Button>
)}
{m.active && m.type === 'password' && (
<Button size="small" variant="outlined" onClick={() => setShowChangePassword(true)}>
Сменить пароль
</Button>
)}
{!m.active && m.type === 'password' && (
<Button size="small" variant="outlined" onClick={() => setShowSetPassword(true)}>
Установить пароль
</Button>
)}
{!m.active && m.type !== 'password' && (
<Button
size="small"
variant="outlined"
component="a"
href={`/api/auth/oauth/${m.type}/link?token=${localStorage.getItem('craftshop_auth_token') || ''}`}
>
Привязать
</Button>
)}
</Stack>
))}
</Stack>
{showSetPassword && (
<Stack spacing={2} sx={{ mt: 2, p: 2, border: '1px solid', borderColor: 'divider', borderRadius: 1 }}>
<TextField label="Пароль" type="password" {...passwordForm.register('password')} fullWidth />
<TextField
label="Подтверждение пароля"
type="password"
{...passwordForm.register('passwordConfirm')}
fullWidth
error={
Boolean(passwordForm.watch('passwordConfirm')) &&
passwordForm.watch('password') !== passwordForm.watch('passwordConfirm')
}
helperText={
passwordForm.watch('passwordConfirm') &&
passwordForm.watch('password') !== passwordForm.watch('passwordConfirm')
? 'Пароли не совпадают'
: null
}
/>
<Stack direction="row" spacing={1}>
<Button
variant="contained"
disabled={
!passwordForm.watch('password') ||
passwordForm.watch('password').length < 8 ||
passwordForm.watch('password') !== passwordForm.watch('passwordConfirm') ||
setPasswordMutation.isPending
}
onClick={() => setPasswordMutation.mutate(passwordForm.getValues('password'))}
>
Сохранить
</Button>
<Button variant="text" onClick={() => setShowSetPassword(false)}>
Отмена
</Button>
</Stack>
</Stack>
)}
{showChangePassword && (
<Stack spacing={2} sx={{ mt: 2, p: 2, border: '1px solid', borderColor: 'divider', borderRadius: 1 }}>
<TextField label="Текущий пароль" type="password" {...changePasswordForm.register('oldPassword')} fullWidth />
<TextField label="Новый пароль" type="password" {...changePasswordForm.register('newPassword')} fullWidth />
<TextField
label="Подтверждение пароля"
type="password"
{...changePasswordForm.register('confirmPassword')}
fullWidth
error={
Boolean(changePasswordForm.watch('confirmPassword')) &&
changePasswordForm.watch('newPassword') !== changePasswordForm.watch('confirmPassword')
}
helperText={
changePasswordForm.watch('confirmPassword') &&
changePasswordForm.watch('newPassword') !== changePasswordForm.watch('confirmPassword')
? 'Пароли не совпадают'
: null
}
/>
<Stack direction="row" spacing={1}>
<Button
variant="contained"
disabled={
!changePasswordForm.watch('oldPassword') ||
!changePasswordForm.watch('newPassword') ||
changePasswordForm.watch('newPassword').length < 8 ||
changePasswordForm.watch('newPassword') !== changePasswordForm.watch('confirmPassword') ||
changePasswordMutation.isPending
}
onClick={() =>
changePasswordMutation.mutate({
oldPassword: changePasswordForm.getValues('oldPassword'),
newPassword: changePasswordForm.getValues('newPassword'),
})
}
>
Сохранить
</Button>
<Button variant="text" onClick={() => setShowChangePassword(false)}>
Отмена
</Button>
</Stack>
</Stack>
)}
</Box>
)
}