# 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)
