diff --git a/server/src/routes/oauth-social.js b/server/src/routes/oauth-social.js index dba2db1..8eeea59 100644 --- a/server/src/routes/oauth-social.js +++ b/server/src/routes/oauth-social.js @@ -17,7 +17,7 @@ async function issueUserJwt(fastify, userId, email) { return fastify.jwt.sign({ sub: userId, email }) } -async function findOrCreateUserFromOAuth({ provider, providerUserId, accessToken, suggestedEmail }) { +async function findOrCreateUserFromOAuth({ provider, providerUserId, accessToken, suggestedEmail, linkToUserId }) { const existingLink = await prisma.oAuthAccount.findUnique({ where: { provider_providerUserId: { provider, providerUserId } }, include: { user: true }, @@ -34,6 +34,15 @@ async function findOrCreateUserFromOAuth({ provider, providerUserId, accessToken const trimmed = typeof suggestedEmail === 'string' ? suggestedEmail.trim() : '' const norm = trimmed ? normalizeEmail(trimmed) : null + + if (linkToUserId) { + if (!norm) return null + await prisma.oAuthAccount.create({ + data: { provider, providerUserId: String(providerUserId), userId: linkToUserId, accessToken }, + }) + return prisma.user.findUnique({ where: { id: linkToUserId } }) + } + let user = norm ? await prisma.user.findUnique({ where: { email: norm } }) : null if (user) { await prisma.oAuthAccount.create({ @@ -42,16 +51,22 @@ async function findOrCreateUserFromOAuth({ provider, providerUserId, accessToken return user } - let email = norm || `${provider}_${providerUserId}@oauth.craftshop.local` - let n = 0 - while (await prisma.user.findUnique({ where: { email } })) { - n += 1 - email = `${provider}_${providerUserId}_${n}@oauth.craftshop.local` - } - user = await prisma.user.create({ data: { email } }) + if (!norm) return null + + user = await prisma.user.create({ + data: { + email: norm, + displayName: norm.split('@')[0], + avatar: null, + avatarStyle: 'avataaars', + }, + }) await prisma.oAuthAccount.create({ data: { provider, providerUserId: String(providerUserId), userId: user.id, accessToken }, }) + await prisma.notificationPreference.create({ + data: { userId: user.id, globalEnabled: true }, + }) return user } @@ -85,9 +100,10 @@ export async function registerOAuthSocialRoutes(fastify) { return oauthErrorRedirect(reply, String(query.error_description || query.error || 'ошибка VK')) } + let statePayload = null try { - const state = typeof query.state === 'string' ? query.state : '' - fastify.jwt.verify(state || '') + const raw = typeof query.state === 'string' ? query.state : '' + statePayload = fastify.jwt.verify(raw || '') } catch { return oauthErrorRedirect(reply, 'Недействительный state OAuth') } @@ -114,50 +130,26 @@ export async function registerOAuthSocialRoutes(fastify) { const vkUserId = tokenBody?.user_id const accessTokenVk = tokenBody?.access_token - let emailSuggestion = typeof tokenBody?.email === 'string' ? tokenBody.email : null - let firstName = null - let lastName = null - let gender = null - let avatar = null - try { - if (accessTokenVk && vkUserId) { - const u = new URL('https://api.vk.com/method/users.get') - u.searchParams.set('access_token', accessTokenVk) - u.searchParams.set('users_ids', String(vkUserId)) - u.searchParams.set('fields', 'photo_200,sex') - u.searchParams.set('v', '5.199') - const profRes = await fetch(u.toString()) - const prof = await profRes.json() - const u0 = prof?.response?.[0] - if (u0) { - firstName = u0.first_name ?? null - lastName = u0.last_name ?? null - avatar = u0.photo_200 ?? null - if (u0.sex === 1) gender = 'female' - else if (u0.sex === 2) gender = 'male' - } - } - } catch { - // ignore profile extras - } + const emailSuggestion = typeof tokenBody?.email === 'string' ? tokenBody.email : null + + if (!emailSuggestion) return oauthErrorRedirect(reply, 'no_email') + + const linkToUserId = statePayload?.action === 'link' ? statePayload.userId : undefined const user = await findOrCreateUserFromOAuth({ provider: 'vk', providerUserId: String(vkUserId), accessToken: accessTokenVk ?? null, suggestedEmail: emailSuggestion, + linkToUserId, }) - const displayName = [firstName, lastName].filter(Boolean).join(' ').trim() - const updateData = {} - if (displayName && !user.displayName) updateData.displayName = displayName - if (firstName) updateData.firstName = firstName - if (lastName) updateData.lastName = lastName - if (gender) updateData.gender = gender - if (avatar) updateData.avatar = avatar - if (Object.keys(updateData).length > 0) { - await prisma.user.update({ where: { id: user.id }, data: updateData }) + if (!user) return oauthErrorRedirect(reply, 'Не удалось получить email от VK') + + if (linkToUserId) { + const base = process.env.CLIENT_PUBLIC_URL || 'http://127.0.0.1:5173' + return reply.redirect(`${base.replace(/\/$/, '')}/me?linked=vk`) } const token = await issueUserJwt(fastify, user.id, user.email) @@ -176,7 +168,7 @@ export async function registerOAuthSocialRoutes(fastify) { url.searchParams.set('response_type', 'code') url.searchParams.set('client_id', clientId) url.searchParams.set('redirect_uri', redirectUri) - url.searchParams.set('scope', 'login:email login:info') + url.searchParams.set('scope', 'login:email') url.searchParams.set('state', state) return reply.redirect(url.toString()) @@ -186,9 +178,10 @@ export async function registerOAuthSocialRoutes(fastify) { const query = request.query ?? {} if (query.error) return oauthErrorRedirect(reply, String(query.error)) + let statePayload = null try { - const state = typeof query.state === 'string' ? query.state : '' - fastify.jwt.verify(state || '') + const raw = typeof query.state === 'string' ? query.state : '' + statePayload = fastify.jwt.verify(raw || '') } catch { return oauthErrorRedirect(reply, 'Недействительный state OAuth') } @@ -233,27 +226,25 @@ export async function registerOAuthSocialRoutes(fastify) { const emailGuess = (Array.isArray(info?.emails) && info.emails[0]) || info?.default_email || - (info?.login ? `${info.login}@yandex.ru` : null) + null + + if (!emailGuess) return oauthErrorRedirect(reply, 'no_email') + + const linkToUserId = statePayload?.action === 'link' ? statePayload.userId : undefined const user = await findOrCreateUserFromOAuth({ provider: 'yandex', providerUserId: yaUserId, accessToken: yaToken, - suggestedEmail: emailGuess || null, + suggestedEmail: emailGuess, + linkToUserId, }) - const updateData = {} - const displayName = - [info.first_name, info.last_name].filter(Boolean).join(' ').trim() || info.display_name || info.real_name - if (displayName && !user.displayName) updateData.displayName = displayName - if (info.first_name) updateData.firstName = info.first_name - if (info.last_name) updateData.lastName = info.last_name - if (info.sex === 'male' || info.sex === 'female') updateData.gender = info.sex - if (info.default_avatar_id && !info.is_avatar_empty) { - updateData.avatar = `https://avatars.yandex.net/get-yapic/${info.default_avatar_id}/islands-200` - } - if (Object.keys(updateData).length > 0) { - await prisma.user.update({ where: { id: user.id }, data: updateData }) + if (!user) return oauthErrorRedirect(reply, 'Не удалось получить email от Яндекс') + + if (linkToUserId) { + const base = process.env.CLIENT_PUBLIC_URL || 'http://127.0.0.1:5173' + return reply.redirect(`${base.replace(/\/$/, '')}/me?linked=yandex`) } const token = await issueUserJwt(fastify, user.id, user.email)