From 13cc1fa2b81c8198cfc7e5c3ae9598198b923041 Mon Sep 17 00:00:00 2001 From: Kirill Date: Fri, 22 May 2026 22:51:03 +0500 Subject: [PATCH] docs: add VK no-email fix design spec --- .../2026-05-22-vk-no-email-fix-design.md | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-22-vk-no-email-fix-design.md diff --git a/docs/superpowers/specs/2026-05-22-vk-no-email-fix-design.md b/docs/superpowers/specs/2026-05-22-vk-no-email-fix-design.md new file mode 100644 index 0000000..f1bd362 --- /dev/null +++ b/docs/superpowers/specs/2026-05-22-vk-no-email-fix-design.md @@ -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_@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)