# VK OAuth без email — Design ## Проблема VK ID не всегда возвращает email (необязательное поле). Текущий код требует email на трёх уровнях: 1. Callback (`oauth-social.js:209`) — `if (!emailSuggestion) return oauthErrorRedirect(...)` 2. `findOrCreateUserFromOAuth` (`oauth-social.js:72,87`) — `if (!norm) return null` 3. Схема БД — `email String @unique` (NOT NULL) Результат: пользователь, у которого VK не отдал email, видит ошибку `no_email` и не может войти. ## Решение Три изменения: 1. **Новый пользователь без email** — генерировать синтетический email `vk_@vk.local` 2. **Привязка VK к существующему аккаунту (link)** — не требовать email от VK 3. **Смена 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_@vk.local` и создаём пользователя с `displayName = 'Пользователь'`. **VK callback** (стр. 206-209): убрать строку `if (!emailSuggestion) return oauthErrorRedirect(reply, 'no_email')`. **Yandex callback** — без изменений (Яндекс всегда возвращает email). --- ## Часть 2: Смена email с верификацией ### Схема БД — новая модель `PendingEmail` ```prisma 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) } ``` ### Миграция ```bash 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=' }` (отправка 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`** — добавить эффекты: ```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)