chore: fix lint issues, remove unused hasAvatar

This commit is contained in:
Kirill
2026-05-22 12:27:20 +05:00
parent 5651403d2e
commit b2ccc2a256
14 changed files with 113 additions and 101 deletions
@@ -43,12 +43,7 @@ export function UserMenu({ user, isAdmin = false, onNavigate, onLogout }: Props)
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }} anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
> >
{user ? ( {user ? (
<UserAvatar <UserAvatar userId={user.id} avatarUrl={user.avatar} avatarStyle={user.avatarStyle} size={28} />
userId={user.id}
avatarUrl={user.avatar}
avatarStyle={user.avatarStyle}
size={28}
/>
) : ( ) : (
<PersonIcon sx={{ fontSize: 28 }} /> <PersonIcon sx={{ fontSize: 28 }} />
)} )}
@@ -63,11 +63,8 @@ export function AdminSettingsPage() {
const hasUnsavedPreview = previewSrc !== null const hasUnsavedPreview = previewSrc !== null
const profileSaveMut = useMutation({ const profileSaveMut = useMutation({
mutationFn: (params: { mutationFn: (params: { displayName: string | null; avatar?: string | null; avatarStyle?: string | null }) =>
displayName: string | null apiClient.patch('admin/profile', params),
avatar?: string | null
avatarStyle?: string | null
}) => apiClient.patch('admin/profile', params),
onSuccess: (_data, variables) => { onSuccess: (_data, variables) => {
const p: UpdateProfileParams = { displayName: variables.displayName ?? null } const p: UpdateProfileParams = { displayName: variables.displayName ?? null }
if (variables.avatar !== undefined) { if (variables.avatar !== undefined) {
@@ -192,12 +192,7 @@ export function AdminUsersPage() {
users.map((u) => ( users.map((u) => (
<TableRow key={u.id} hover> <TableRow key={u.id} hover>
<TableCell> <TableCell>
<UserAvatar <UserAvatar userId={u.id} avatarUrl={u.avatar} avatarStyle={u.avatarStyle} size={28} />
userId={u.id}
avatarUrl={u.avatar}
avatarStyle={u.avatarStyle}
size={28}
/>
</TableCell> </TableCell>
<TableCell>{u.email}</TableCell> <TableCell>{u.email}</TableCell>
<TableCell>{u.displayName ?? '—'}</TableCell> <TableCell>{u.displayName ?? '—'}</TableCell>
@@ -1,5 +1,5 @@
import { render, screen, fireEvent } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { render, screen, fireEvent } from '@testing-library/react'
import { MemoryRouter } from 'react-router-dom' import { MemoryRouter } from 'react-router-dom'
import { describe, expect, it, vi } from 'vitest' import { describe, expect, it, vi } from 'vitest'
import { AuthPage } from '../ui/AuthPage' import { AuthPage } from '../ui/AuthPage'
+16 -11
View File
@@ -112,8 +112,7 @@ export function AuthPage() {
getApiErrorMessage(requestCode.error) || getApiErrorMessage(requestCode.error) ||
getApiErrorMessage(verifyCode.error) getApiErrorMessage(verifyCode.error)
const passwordError = const passwordError = isRegister && passwordConfirm && password !== passwordConfirm ? 'Пароли не совпадают' : null
isRegister && passwordConfirm && password !== passwordConfirm ? 'Пароли не совпадают' : null
return ( return (
<Box> <Box>
@@ -121,11 +120,21 @@ export function AuthPage() {
Вход / регистрация Вход / регистрация
</Typography> </Typography>
{message && <Alert severity="success" sx={{ mb: 2 }}>{message}</Alert>} {message && (
{oauthError && ( <Alert severity="success" sx={{ mb: 2 }}>
<Alert severity="error" sx={{ mb: 2 }} onClose={() => setOauthError(null)}>{oauthError}</Alert> {message}
</Alert>
)}
{oauthError && (
<Alert severity="error" sx={{ mb: 2 }} onClose={() => setOauthError(null)}>
{oauthError}
</Alert>
)}
{errMsg && (
<Alert severity="error" sx={{ mb: 2 }}>
{errMsg}
</Alert>
)} )}
{errMsg && <Alert severity="error" sx={{ mb: 2 }}>{errMsg}</Alert>}
<Tabs value={tab} onChange={(_, v) => setTab(v)} sx={{ mb: 3 }}> <Tabs value={tab} onChange={(_, v) => setTab(v)} sx={{ mb: 3 }}>
<Tab label="Пароль" /> <Tab label="Пароль" />
@@ -198,11 +207,7 @@ export function AuthPage() {
<Stack spacing={2} sx={{ maxWidth: 520 }}> <Stack spacing={2} sx={{ maxWidth: 520 }}>
<TextField label="Email" {...register('email')} fullWidth /> <TextField label="Email" {...register('email')} fullWidth />
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2}> <Stack direction={{ xs: 'column', sm: 'row' }} spacing={2}>
<Button <Button variant="outlined" onClick={() => requestCode.mutate()} disabled={!email || requestCode.isPending}>
variant="outlined"
onClick={() => requestCode.mutate()}
disabled={!email || requestCode.isPending}
>
Отправить код Отправить код
</Button> </Button>
<TextField label="Код (6 цифр)" inputMode="numeric" {...register('code')} /> <TextField label="Код (6 цифр)" inputMode="numeric" {...register('code')} />
@@ -12,9 +12,9 @@ import Stack from '@mui/material/Stack'
import TextField from '@mui/material/TextField' import TextField from '@mui/material/TextField'
import Typography from '@mui/material/Typography' import Typography from '@mui/material/Typography'
import { createAvatar } from '@dicebear/core' import { createAvatar } from '@dicebear/core'
import { useMutation } from '@tanstack/react-query'
import { useUnit } from 'effector-react' import { useUnit } from 'effector-react'
import { useForm } from 'react-hook-form' 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 { AVATAR_STYLES, DEFAULT_STYLE_ID, getStyleById } from '@/shared/lib/avatar-styles'
import { import {
$requestEmailChangeCodeError, $requestEmailChangeCodeError,
@@ -62,8 +62,6 @@ export function SettingsPage() {
const emailErrorMsg = getApiErrorMessage(errorEmailReq) ?? getApiErrorMessage(errorEmailVerify) const emailErrorMsg = getApiErrorMessage(errorEmailReq) ?? getApiErrorMessage(errorEmailVerify)
const profileErrorMsg = getApiErrorMessage(errorProfile) const profileErrorMsg = getApiErrorMessage(errorProfile)
const hasAvatar = Boolean(user?.avatar)
const [selectedStyle, setSelectedStyle] = useState(user?.avatarStyle || DEFAULT_STYLE_ID) const [selectedStyle, setSelectedStyle] = useState(user?.avatarStyle || DEFAULT_STYLE_ID)
const [previewSrc, setPreviewSrc] = useState<string | null>(null) const [previewSrc, setPreviewSrc] = useState<string | null>(null)
const [previewStyle, setPreviewStyle] = useState<string>(DEFAULT_STYLE_ID) const [previewStyle, setPreviewStyle] = useState<string>(DEFAULT_STYLE_ID)
@@ -77,7 +75,9 @@ export function SettingsPage() {
}) })
useEffect(() => { useEffect(() => {
fetchAuthMethodsFx().then(setAuthMethods).catch(() => { fetchAuthMethodsFx()
.then(setAuthMethods)
.catch(() => {
setAuthMethods([]) setAuthMethods([])
}) })
}, []) }, [])
@@ -277,12 +277,7 @@ export function SettingsPage() {
</Button> </Button>
)} )}
{!m.active && m.type !== 'password' && ( {!m.active && m.type !== 'password' && (
<Button <Button size="small" variant="outlined" component="a" href={`/api/auth/oauth/${m.type}/link`}>
size="small"
variant="outlined"
component="a"
href={`/api/auth/oauth/${m.type}/link`}
>
Привязать Привязать
</Button> </Button>
)} )}
+2 -4
View File
@@ -92,13 +92,11 @@ export const loginFx = createEffect(async (params: { email: string; password: st
return data.user return data.user
}) })
export const registerFx = createEffect( export const registerFx = createEffect(async (params: { email: string; password: string; displayName?: string }) => {
async (params: { email: string; password: string; displayName?: string }) => {
const { data } = await apiClient.post<{ token: string; user: AuthUser }>('auth/register', params) const { data } = await apiClient.post<{ token: string; user: AuthUser }>('auth/register', params)
tokenSet(data.token) tokenSet(data.token)
return data.user return data.user
}, })
)
export const fetchAuthMethodsFx = createEffect(async () => { export const fetchAuthMethodsFx = createEffect(async () => {
const { data } = await apiClient.get<{ methods: AuthMethod[] }>('me/auth-methods') const { data } = await apiClient.get<{ methods: AuthMethod[] }>('me/auth-methods')
Binary file not shown.
@@ -1,5 +1,5 @@
import Fastify from 'fastify'
import jwt from '@fastify/jwt' import jwt from '@fastify/jwt'
import Fastify from 'fastify'
import { afterAll, beforeEach, beforeAll, describe, expect, it } from 'vitest' import { afterAll, beforeEach, beforeAll, describe, expect, it } from 'vitest'
import { prisma } from '../../lib/prisma.js' import { prisma } from '../../lib/prisma.js'
import { registerAuthRoutes } from '../auth.js' import { registerAuthRoutes } from '../auth.js'
@@ -10,7 +10,9 @@ async function buildApp() {
const app = Fastify({ logger: false }) const app = Fastify({ logger: false })
await app.register(jwt, { secret: JWT_SECRET }) await app.register(jwt, { secret: JWT_SECRET })
app.decorate('authenticate', async function (request, reply) { app.decorate('authenticate', async function (request, reply) {
try { await request.jwtVerify() } catch { try {
await request.jwtVerify()
} catch {
return reply.code(401).send({ error: 'Unauthorized' }) return reply.code(401).send({ error: 'Unauthorized' })
} }
}) })
@@ -36,7 +38,9 @@ describe('GET /api/me/auth-methods', () => {
let app, user, token let app, user, token
const email = `test-methods-${Date.now()}@example.com` const email = `test-methods-${Date.now()}@example.com`
beforeAll(async () => { app = await buildApp() }) beforeAll(async () => {
app = await buildApp()
})
afterAll(async () => { afterAll(async () => {
await prisma.notificationPreference.deleteMany({ where: { userId: user?.id } }) await prisma.notificationPreference.deleteMany({ where: { userId: user?.id } })
await prisma.user.deleteMany({ where: { email } }) await prisma.user.deleteMany({ where: { email } })
@@ -53,7 +57,8 @@ describe('GET /api/me/auth-methods', () => {
it('returns methods for user without any method', async () => { it('returns methods for user without any method', async () => {
const res = await app.inject({ const res = await app.inject({
method: 'GET', url: '/api/me/auth-methods', method: 'GET',
url: '/api/me/auth-methods',
headers: { authorization: `Bearer ${token}` }, headers: { authorization: `Bearer ${token}` },
}) })
expect(res.statusCode).toBe(200) expect(res.statusCode).toBe(200)
@@ -66,7 +71,8 @@ describe('GET /api/me/auth-methods', () => {
it('returns password as active after setting it', async () => { it('returns password as active after setting it', async () => {
await prisma.user.update({ where: { id: user.id }, data: { passwordHash: 'hashed' } }) await prisma.user.update({ where: { id: user.id }, data: { passwordHash: 'hashed' } })
const res = await app.inject({ const res = await app.inject({
method: 'GET', url: '/api/me/auth-methods', method: 'GET',
url: '/api/me/auth-methods',
headers: { authorization: `Bearer ${token}` }, headers: { authorization: `Bearer ${token}` },
}) })
expect(JSON.parse(res.body).methods.find((m) => m.type === 'password').active).toBe(true) expect(JSON.parse(res.body).methods.find((m) => m.type === 'password').active).toBe(true)
@@ -77,7 +83,9 @@ describe('POST /api/me/password', () => {
let app, user, token let app, user, token
const email = `test-set-pw-${Date.now()}@example.com` const email = `test-set-pw-${Date.now()}@example.com`
beforeAll(async () => { app = await buildApp() }) beforeAll(async () => {
app = await buildApp()
})
afterAll(async () => { afterAll(async () => {
await prisma.notificationPreference.deleteMany({ where: { userId: user?.id } }) await prisma.notificationPreference.deleteMany({ where: { userId: user?.id } })
await prisma.user.deleteMany({ where: { email } }) await prisma.user.deleteMany({ where: { email } })
@@ -93,7 +101,8 @@ describe('POST /api/me/password', () => {
it('sets password', async () => { it('sets password', async () => {
const res = await app.inject({ const res = await app.inject({
method: 'POST', url: '/api/me/password', method: 'POST',
url: '/api/me/password',
headers: { authorization: `Bearer ${token}` }, headers: { authorization: `Bearer ${token}` },
payload: { password: 'Test123!@' }, payload: { password: 'Test123!@' },
}) })
@@ -106,7 +115,8 @@ describe('POST /api/me/password', () => {
it('rejects if password already set', async () => { it('rejects if password already set', async () => {
await prisma.user.update({ where: { id: user.id }, data: { passwordHash: 'existing' } }) await prisma.user.update({ where: { id: user.id }, data: { passwordHash: 'existing' } })
const res = await app.inject({ const res = await app.inject({
method: 'POST', url: '/api/me/password', method: 'POST',
url: '/api/me/password',
headers: { authorization: `Bearer ${token}` }, headers: { authorization: `Bearer ${token}` },
payload: { password: 'Test123!@' }, payload: { password: 'Test123!@' },
}) })
@@ -118,7 +128,9 @@ describe('DELETE /api/me/oauth/:provider', () => {
let app, user, token let app, user, token
const email = `test-unlink-${Date.now()}@example.com` const email = `test-unlink-${Date.now()}@example.com`
beforeAll(async () => { app = await buildApp() }) beforeAll(async () => {
app = await buildApp()
})
afterAll(async () => { afterAll(async () => {
await prisma.oAuthAccount.deleteMany({ where: { user: { email } } }) await prisma.oAuthAccount.deleteMany({ where: { user: { email } } })
await prisma.notificationPreference.deleteMany({ where: { user: { email } } }) await prisma.notificationPreference.deleteMany({ where: { user: { email } } })
@@ -135,7 +147,8 @@ describe('DELETE /api/me/oauth/:provider', () => {
it('returns 404 for non-linked provider', async () => { it('returns 404 for non-linked provider', async () => {
const res = await app.inject({ const res = await app.inject({
method: 'DELETE', url: '/api/me/oauth/vk', method: 'DELETE',
url: '/api/me/oauth/vk',
headers: { authorization: `Bearer ${token}` }, headers: { authorization: `Bearer ${token}` },
}) })
expect(res.statusCode).toBe(404) expect(res.statusCode).toBe(404)
@@ -147,7 +160,8 @@ describe('DELETE /api/me/oauth/:provider', () => {
data: { provider: 'vk', providerUserId: '123', userId: user.id }, data: { provider: 'vk', providerUserId: '123', userId: user.id },
}) })
const res = await app.inject({ const res = await app.inject({
method: 'DELETE', url: '/api/me/oauth/vk', method: 'DELETE',
url: '/api/me/oauth/vk',
headers: { authorization: `Bearer ${token}` }, headers: { authorization: `Bearer ${token}` },
}) })
expect(res.statusCode).toBe(200) expect(res.statusCode).toBe(200)
@@ -161,7 +175,8 @@ describe('DELETE /api/me/oauth/:provider', () => {
data: { provider: 'vk', providerUserId: '123', userId: user.id }, data: { provider: 'vk', providerUserId: '123', userId: user.id },
}) })
const res = await app.inject({ const res = await app.inject({
method: 'DELETE', url: '/api/me/oauth/vk', method: 'DELETE',
url: '/api/me/oauth/vk',
headers: { authorization: `Bearer ${token}` }, headers: { authorization: `Bearer ${token}` },
}) })
expect(res.statusCode).toBe(400) expect(res.statusCode).toBe(400)
@@ -1,5 +1,5 @@
import Fastify from 'fastify'
import jwt from '@fastify/jwt' import jwt from '@fastify/jwt'
import Fastify from 'fastify'
import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest' import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest'
import { prisma } from '../../lib/prisma.js' import { prisma } from '../../lib/prisma.js'
import { registerAuthRoutes } from '../auth.js' import { registerAuthRoutes } from '../auth.js'
@@ -26,8 +26,12 @@ async function buildApp() {
describe('POST /api/auth/register', () => { describe('POST /api/auth/register', () => {
let app let app
beforeAll(async () => { app = await buildApp() }) beforeAll(async () => {
afterAll(async () => { await app.close() }) app = await buildApp()
})
afterAll(async () => {
await app.close()
})
afterEach(async () => { afterEach(async () => {
await prisma.authCode.deleteMany({ where: { email: TEST_EMAIL } }) await prisma.authCode.deleteMany({ where: { email: TEST_EMAIL } })
await prisma.notificationPreference.deleteMany({ where: { user: { email: TEST_EMAIL } } }) await prisma.notificationPreference.deleteMany({ where: { user: { email: TEST_EMAIL } } })
@@ -48,11 +52,13 @@ describe('POST /api/auth/register', () => {
it('rejects duplicate email', async () => { it('rejects duplicate email', async () => {
await app.inject({ await app.inject({
method: 'POST', url: '/api/auth/register', method: 'POST',
url: '/api/auth/register',
payload: { email: TEST_EMAIL, password: 'Test123!@' }, payload: { email: TEST_EMAIL, password: 'Test123!@' },
}) })
const res = await app.inject({ const res = await app.inject({
method: 'POST', url: '/api/auth/register', method: 'POST',
url: '/api/auth/register',
payload: { email: TEST_EMAIL, password: 'Test123!@' }, payload: { email: TEST_EMAIL, password: 'Test123!@' },
}) })
expect(res.statusCode).toBe(409) expect(res.statusCode).toBe(409)
@@ -60,7 +66,8 @@ describe('POST /api/auth/register', () => {
it('rejects weak password — too short', async () => { it('rejects weak password — too short', async () => {
const res = await app.inject({ const res = await app.inject({
method: 'POST', url: '/api/auth/register', method: 'POST',
url: '/api/auth/register',
payload: { email: TEST_EMAIL, password: 'Ab1!' }, payload: { email: TEST_EMAIL, password: 'Ab1!' },
}) })
expect(res.statusCode).toBe(400) expect(res.statusCode).toBe(400)
@@ -70,7 +77,8 @@ describe('POST /api/auth/register', () => {
it('rejects weak password — no digit', async () => { it('rejects weak password — no digit', async () => {
const res = await app.inject({ const res = await app.inject({
method: 'POST', url: '/api/auth/register', method: 'POST',
url: '/api/auth/register',
payload: { email: TEST_EMAIL, password: 'Abcdefgh!' }, payload: { email: TEST_EMAIL, password: 'Abcdefgh!' },
}) })
expect(res.statusCode).toBe(400) expect(res.statusCode).toBe(400)
@@ -79,7 +87,8 @@ describe('POST /api/auth/register', () => {
it('rejects weak password — no special char', async () => { it('rejects weak password — no special char', async () => {
const res = await app.inject({ const res = await app.inject({
method: 'POST', url: '/api/auth/register', method: 'POST',
url: '/api/auth/register',
payload: { email: TEST_EMAIL, password: 'Abcdefg1' }, payload: { email: TEST_EMAIL, password: 'Abcdefg1' },
}) })
expect(res.statusCode).toBe(400) expect(res.statusCode).toBe(400)
@@ -92,7 +101,8 @@ describe('POST /api/auth/login', () => {
beforeAll(async () => { beforeAll(async () => {
app = await buildApp() app = await buildApp()
await app.inject({ await app.inject({
method: 'POST', url: '/api/auth/register', method: 'POST',
url: '/api/auth/register',
payload: { email: LOGIN_EMAIL, password: 'Test123!@' }, payload: { email: LOGIN_EMAIL, password: 'Test123!@' },
}) })
}) })
@@ -106,7 +116,8 @@ describe('POST /api/auth/login', () => {
it('logs in with correct password', async () => { it('logs in with correct password', async () => {
const res = await app.inject({ const res = await app.inject({
method: 'POST', url: '/api/auth/login', method: 'POST',
url: '/api/auth/login',
payload: { email: LOGIN_EMAIL, password: 'Test123!@' }, payload: { email: LOGIN_EMAIL, password: 'Test123!@' },
headers: { 'x-forwarded-for': '1.1.1.1' }, headers: { 'x-forwarded-for': '1.1.1.1' },
}) })
@@ -116,7 +127,8 @@ describe('POST /api/auth/login', () => {
it('rejects wrong password', async () => { it('rejects wrong password', async () => {
const res = await app.inject({ const res = await app.inject({
method: 'POST', url: '/api/auth/login', method: 'POST',
url: '/api/auth/login',
payload: { email: LOGIN_EMAIL, password: 'Wrong!!1!' }, payload: { email: LOGIN_EMAIL, password: 'Wrong!!1!' },
headers: { 'x-forwarded-for': '2.2.2.2' }, headers: { 'x-forwarded-for': '2.2.2.2' },
}) })
@@ -125,7 +137,8 @@ describe('POST /api/auth/login', () => {
it('rejects non-existent email', async () => { it('rejects non-existent email', async () => {
const res = await app.inject({ const res = await app.inject({
method: 'POST', url: '/api/auth/login', method: 'POST',
url: '/api/auth/login',
payload: { email: 'nobody@nowhere.test', password: 'Test123!@' }, payload: { email: 'nobody@nowhere.test', password: 'Test123!@' },
headers: { 'x-forwarded-for': '3.3.3.3' }, headers: { 'x-forwarded-for': '3.3.3.3' },
}) })
+1 -1
View File
@@ -1,10 +1,10 @@
import { mapProductForApi, parseMaterialsInput, slugify } from './api/_product-helpers.js' import { mapProductForApi, parseMaterialsInput, slugify } from './api/_product-helpers.js'
import { registerAdminNotificationRoutes } from './api/admin/notifications.js' import { registerAdminNotificationRoutes } from './api/admin/notifications.js'
import { registerAdminProfileRoutes } from './api/admin-profile.js'
import { registerAdminCategoryRoutes } from './api/admin-categories.js' import { registerAdminCategoryRoutes } from './api/admin-categories.js'
import { registerAdminGalleryRoutes } from './api/admin-gallery.js' import { registerAdminGalleryRoutes } from './api/admin-gallery.js'
import { registerAdminOrderRoutes } from './api/admin-orders.js' import { registerAdminOrderRoutes } from './api/admin-orders.js'
import { registerAdminProductRoutes } from './api/admin-products.js' import { registerAdminProductRoutes } from './api/admin-products.js'
import { registerAdminProfileRoutes } from './api/admin-profile.js'
import { registerAdminReviewRoutes } from './api/admin-reviews.js' import { registerAdminReviewRoutes } from './api/admin-reviews.js'
import { registerAdminUserRoutes } from './api/admin-users.js' import { registerAdminUserRoutes } from './api/admin-users.js'
import { registerCatalogSliderRoutes } from './api/catalog-slider.js' import { registerCatalogSliderRoutes } from './api/catalog-slider.js'
+3 -1
View File
@@ -73,7 +73,9 @@ export async function registerAdminOrderRoutes(fastify) {
const order = await prisma.order.findUnique({ const order = await prisma.order.findUnique({
where: { id }, where: { id },
include: { include: {
user: { select: { id: true, email: true, displayName: true, avatar: true, avatarType: true, avatarStyle: true } }, user: {
select: { id: true, email: true, displayName: true, avatar: true, avatarType: true, avatarStyle: true },
},
items: true, items: true,
messages: { orderBy: { createdAt: 'asc' } }, messages: { orderBy: { createdAt: 'asc' } },
}, },
+3 -1
View File
@@ -87,7 +87,9 @@ export async function registerPublicReviewRoutes(fastify) {
const total = await prisma.review.count({ where }) const total = await prisma.review.count({ where })
const rawItems = await prisma.review.findMany({ const rawItems = await prisma.review.findMany({
where, where,
include: { user: { select: { email: true, displayName: true, avatar: true, avatarType: true, avatarStyle: true } } }, include: {
user: { select: { email: true, displayName: true, avatar: true, avatarType: true, avatarStyle: true } },
},
orderBy: { createdAt: 'desc' }, orderBy: { createdAt: 'desc' },
skip: (page - 1) * pageSize, skip: (page - 1) * pageSize,
take: pageSize, take: pageSize,
+13 -18
View File
@@ -105,10 +105,7 @@ export async function registerOAuthSocialRoutes(fastify) {
if (!clientId || !clientSecret) return reply.code(503).send({ error: 'VK OAuth не настроен' }) if (!clientId || !clientSecret) return reply.code(503).send({ error: 'VK OAuth не настроен' })
const redirectUri = `${serverPublic}/api/auth/oauth/vk/callback` const redirectUri = `${serverPublic}/api/auth/oauth/vk/callback`
const state = fastify.jwt.sign( const state = fastify.jwt.sign({ oauth: 'vk', action: 'link', userId: request.user.sub }, { expiresIn: '15m' })
{ oauth: 'vk', action: 'link', userId: request.user.sub },
{ expiresIn: '15m' },
)
const url = new URL('https://oauth.vk.com/authorize') const url = new URL('https://oauth.vk.com/authorize')
url.searchParams.set('client_id', clientId) url.searchParams.set('client_id', clientId)
@@ -128,13 +125,15 @@ export async function registerOAuthSocialRoutes(fastify) {
return oauthErrorRedirect(reply, String(query.error_description || query.error || 'ошибка VK')) return oauthErrorRedirect(reply, String(query.error_description || query.error || 'ошибка VK'))
} }
let statePayload = null const statePayload = (() => {
try { try {
const raw = typeof query.state === 'string' ? query.state : '' const raw = typeof query.state === 'string' ? query.state : ''
statePayload = fastify.jwt.verify(raw || '') return fastify.jwt.verify(raw || '')
} catch { } catch {
return oauthErrorRedirect(reply, 'Недействительный state OAuth') return null
} }
})()
if (!statePayload) return oauthErrorRedirect(reply, 'Недействительный state OAuth')
const code = typeof query.code === 'string' ? query.code.trim() : '' const code = typeof query.code === 'string' ? query.code.trim() : ''
if (!code) return oauthErrorRedirect(reply, 'Не получен код от VK') if (!code) return oauthErrorRedirect(reply, 'Не получен код от VK')
@@ -212,10 +211,7 @@ export async function registerOAuthSocialRoutes(fastify) {
if (!clientId) return reply.code(503).send({ error: 'Yandex OAuth не настроен' }) if (!clientId) return reply.code(503).send({ error: 'Yandex OAuth не настроен' })
const redirectUri = `${serverPublic}/api/auth/oauth/yandex/callback` const redirectUri = `${serverPublic}/api/auth/oauth/yandex/callback`
const state = fastify.jwt.sign( const state = fastify.jwt.sign({ oauth: 'yandex', action: 'link', userId: request.user.sub }, { expiresIn: '15m' })
{ oauth: 'yandex', action: 'link', userId: request.user.sub },
{ expiresIn: '15m' },
)
const url = new URL('https://oauth.yandex.ru/authorize') const url = new URL('https://oauth.yandex.ru/authorize')
url.searchParams.set('response_type', 'code') url.searchParams.set('response_type', 'code')
@@ -231,13 +227,15 @@ export async function registerOAuthSocialRoutes(fastify) {
const query = request.query ?? {} const query = request.query ?? {}
if (query.error) return oauthErrorRedirect(reply, String(query.error)) if (query.error) return oauthErrorRedirect(reply, String(query.error))
let statePayload = null const statePayload = (() => {
try { try {
const raw = typeof query.state === 'string' ? query.state : '' const raw = typeof query.state === 'string' ? query.state : ''
statePayload = fastify.jwt.verify(raw || '') return fastify.jwt.verify(raw || '')
} catch { } catch {
return oauthErrorRedirect(reply, 'Недействительный state OAuth') return null
} }
})()
if (!statePayload) return oauthErrorRedirect(reply, 'Недействительный state OAuth')
const code = typeof query.code === 'string' ? query.code.trim() : '' const code = typeof query.code === 'string' ? query.code.trim() : ''
if (!code) return oauthErrorRedirect(reply, 'Не получен код от Яндекс') if (!code) return oauthErrorRedirect(reply, 'Не получен код от Яндекс')
@@ -276,10 +274,7 @@ export async function registerOAuthSocialRoutes(fastify) {
const yaUserId = String(info?.id || '') const yaUserId = String(info?.id || '')
if (!yaUserId) return oauthErrorRedirect(reply, 'Не удалось получить профиль Yandex') if (!yaUserId) return oauthErrorRedirect(reply, 'Не удалось получить профиль Yandex')
const emailGuess = const emailGuess = (Array.isArray(info?.emails) && info.emails[0]) || info?.default_email || null
(Array.isArray(info?.emails) && info.emails[0]) ||
info?.default_email ||
null
if (!emailGuess) return oauthErrorRedirect(reply, 'no_email') if (!emailGuess) return oauthErrorRedirect(reply, 'no_email')