feat(client): add auth methods section to settings page

This commit is contained in:
Kirill
2026-05-22 12:16:58 +05:00
parent afc763c522
commit 6d23aafcc1
2 changed files with 134 additions and 8 deletions
@@ -53,12 +53,7 @@ export function OrderChat({ messages, isPending, onSend }: Props) {
const isAdminMsg = m.authorType === 'admin'
const adminAv = adminAvatarQuery.data
const avatarNode = isAdminMsg ? (
<UserAvatar
userId="admin"
avatarUrl={adminAv?.avatar}
avatarStyle={adminAv?.avatarStyle}
size={24}
/>
<UserAvatar userId="admin" avatarUrl={adminAv?.avatar} avatarStyle={adminAv?.avatarStyle} size={24} />
) : currentUser ? (
<UserAvatar
userId={currentUser.id}
@@ -1,7 +1,8 @@
import { useState } from 'react'
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 Divider from '@mui/material/Divider'
import FormControl from '@mui/material/FormControl'
import InputLabel from '@mui/material/InputLabel'
@@ -13,15 +14,20 @@ import Typography from '@mui/material/Typography'
import { createAvatar } from '@dicebear/core'
import { useUnit } from 'effector-react'
import { useForm } from 'react-hook-form'
import { useMutation } from '@tanstack/react-query'
import { AVATAR_STYLES, DEFAULT_STYLE_ID, getStyleById } from '@/shared/lib/avatar-styles'
import {
$requestEmailChangeCodeError,
$updateProfileError,
$user,
$verifyEmailChangeError,
fetchAuthMethodsFx,
requestEmailChangeCodeFx,
setPasswordFx,
unlinkOAuthFx,
updateProfileFx,
verifyEmailChangeFx,
type AuthMethod,
} from '@/shared/model/auth'
import { UserAvatar } from '@/shared/ui/UserAvatar'
import type { AxiosError } from 'axios'
@@ -64,6 +70,43 @@ export function SettingsPage() {
const hasUnsavedPreview = previewSrc !== null
const [authMethods, setAuthMethods] = useState<AuthMethod[]>([])
const [showSetPassword, setShowSetPassword] = useState(false)
const passwordForm = useForm<{ password: string; passwordConfirm: string }>({
defaultValues: { password: '', passwordConfirm: '' },
})
useEffect(() => {
fetchAuthMethodsFx().then(setAuthMethods).catch(() => {
setAuthMethods([])
})
}, [])
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 linkedCount = useCallback(() => {
return authMethods.filter((m) => m.active).length
}, [authMethods])
const METHOD_LABELS: Record<string, string> = { password: 'Пароль', vk: 'ВКонтакте', yandex: 'Яндекс' }
if (!user) {
return <Alert severity="info">Нужно войти. Перейдите на страницу «Вход».</Alert>
}
@@ -134,7 +177,7 @@ export function SettingsPage() {
}}
/>
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mt: 0.5 }}>
{hasUnsavedPreview ? 'Предпросмотр' : hasAvatar ? 'Сохранён' : 'Авто'}
{hasUnsavedPreview ? 'Предпросмотр' : user.avatar ? 'Сохранён' : 'Авто'}
</Typography>
</Box>
{hasUnsavedPreview && (
@@ -201,6 +244,94 @@ export function SettingsPage() {
)}
</Box>
{!user.isAdmin && (
<>
<Divider />
<Box>
<Typography variant="h6" gutterBottom>
Методы входа
</Typography>
<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={() => setShowSetPassword(true)}>
Установить пароль
</Button>
)}
{!m.active && m.type !== 'password' && (
<Button
size="small"
variant="outlined"
component="a"
href={`/api/auth/oauth/${m.type}/link`}
>
Привязать
</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>
)}
</Box>
</>
)}
<Divider />
<Box>