5.5 KiB
5.5 KiB
VK OAuth без email — Design
Проблема
VK ID не всегда возвращает email (необязательное поле). Текущий код требует email на трёх уровнях:
- Callback (
oauth-social.js:209) —if (!emailSuggestion) return oauthErrorRedirect(...) findOrCreateUserFromOAuth(oauth-social.js:72,87) —if (!norm) return null- Схема БД —
email String @unique(NOT NULL)
Результат: пользователь, у которого VK не отдал email, видит ошибку no_email и не может войти.
Решение
Три изменения:
- Новый пользователь без email — генерировать синтетический email
vk_<providerUserId>@vk.local - Привязка VK к существующему аккаунту (link) — не требовать email от VK
- Смена email в профиле — дать пользователю возможность сменить синтетический email на настоящий, с верификацией
Часть 1: OAuth flow (сервер)
server/src/routes/oauth-social.js
findOrCreateUserFromOAuth (стр. 53-104):
- Режим link (стр. 71-77): убрать
if (!norm) return null. ЕслиlinkToUserIdпередан — email не нужен, создаёмOAuthAccountи возвращаем пользователя. - Новый пользователь без email (стр. 87): вместо
if (!norm) return null— еслиnormотсутствует, генерируемvk_<providerUserId>@vk.localи создаём пользователя сdisplayName = 'Пользователь'.
VK callback (стр. 206-209): убрать строку if (!emailSuggestion) return oauthErrorRedirect(reply, 'no_email').
Yandex callback — без изменений (Яндекс всегда возвращает email).
Часть 2: Смена email с верификацией
Схема БД — новая модель PendingEmail
model PendingEmail {
id String @id @default(cuid())
userId String
email String
token String @unique
expiresAt DateTime
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
Миграция
cd server && npx prisma migrate dev --name pending_email
Серверные роуты
Новые роуты в server/src/routes/auth-session.js (рядом с /api/me и /api/me/auth-methods):
PATCH /api/me/email (requireAuth):
- Тело:
{ email: string } - Валидация: нормализовать через
normalizeEmail(), проверить формат - Проверить, что email не занят (
findUnique({ email })) → 409 Conflict - Удалить предыдущие
PendingEmailдля этого пользователя - Создать
PendingEmailсtoken = crypto.randomUUID(),expiresAt = now + 24h - Ответ:
{ verificationUrl: '/api/me/verify-email?token=<uuid>' }(отправка email не реализуем, токен возвращаем в ответе API)
GET /api/me/verify-email (без авторизации, только по токену):
- Искать
PendingEmailпо токену, проверитьexpiresAt > now - Обновить
User.email, удалитьPendingEmail - Редирект:
{CLIENT_PUBLIC_URL}/me?emailVerified=1
Клиент
client/src/shared/model/auth.ts — добавить эффекты:
export const requestEmailChangeFx = createEffect(async (email: string) => {
const { data } = await apiClient.patch<{ verificationUrl: string }>('me/email', { email })
return data.verificationUrl
})
export const verifyEmailFx = createEffect(async (token: string) => {
window.location.href = `/api/me/verify-email?token=${token}`
})
AuthMethodsSection.tsx — добавить секцию смены email:
- Текстовое поле (email) + кнопка «Сменить email»
- После успешного запроса — показать кнопку «Подтвердить email» (переход по
verificationUrl) - Ошибки: неверный формат, email занят
- После успешной верификации — обновить
$user(подгрузить черезmeFxзаново)
Структура изменений
server/
prisma/schema.prisma — модель PendingEmail
prisma/migrations/ — миграция (авто)
src/routes/oauth-social.js — findOrCreateUserFromOAuth + VK callback fix
src/routes/auth-session.js — PATCH /api/me/email, GET /api/me/verify-email
__tests__/ — тесты на новый flow
client/
src/shared/model/auth.ts — requestEmailChangeFx, verifyEmailFx
src/pages/me/ui/sections/
AuthMethodsSection.tsx — UI для смены email
__tests__/ — тесты UI
Не входит в scope
- Отправка email с кодом подтверждения (верификация через ссылку в ответе API)
- OAuth для админа (админ только email/код)
- Синтетический email для Яндекса (Яндекс всегда возвращает email)