docs: add VK no-email fix design spec

This commit is contained in:
Kirill
2026-05-22 22:51:03 +05:00
parent f0af519ec1
commit 13cc1fa2b8
@@ -0,0 +1,126 @@
# 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_<providerUserId>@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_<providerUserId>@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=<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`** — добавить эффекты:
```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)