diff --git a/docs/superpowers/specs/2026-05-20-yandex-vk-oauth-design.md b/docs/superpowers/specs/2026-05-20-yandex-vk-oauth-design.md new file mode 100644 index 0000000..5217e5a --- /dev/null +++ b/docs/superpowers/specs/2026-05-20-yandex-vk-oauth-design.md @@ -0,0 +1,92 @@ +# Yandex ID + VK ID OAuth — Design + +## Цель + +Подключить авторизацию через Яндекс ID и VK ID на клиенте (серверная часть OAuth уже реализована). Добавить поля профиля: имя, фамилия, пол, аватар. Админ продолжает входить только через email/код. + +## Объём + +### База данных + +- Переименовать `User.name` → `User.displayName` +- Добавить поля: `firstName String?`, `lastName String?`, `gender String?`, `avatar String?` +- Сбросить БД (`prisma migrate reset --force`), прода нет + +### Сервер + +1. **`server/prisma/schema.prisma`** — обновить модель User +2. **`server/src/routes/oauth-social.js`** — обновить `findOrCreateUserFromOAuth()`: + - Яндекс: сохранять `firstName`, `lastName`, `gender` (`sex`), `avatar` (`https://avatars.yandex.net/get-yapic/{default_avatar_id}/islands-200`), `displayName` ← `real_name` или `display_name` + - VK: сохранять `firstName` (`first_name`), `lastName` (`last_name`), `gender` (`sex`: 1→female, 2→male), `avatar` (`photo_200`), `displayName` ← `first_name + ' ' + last_name` + - Gender: если провайдер не вернул — оставлять `null` +3. **`server/src/routes/auth.js`** — обновить `mapUserForClient()`: добавить новые поля в ответ `/api/me`; переименовать `name` → `displayName` +4. **`server/src/**/*.js`** — найти и заменить все использования `user.name` на `user.displayName` +5. **`server/.env.example`** — документировать redirect URI для Яндекс и VK + +### Клиент + +1. **`client/src/shared/model/auth.ts`** — обновить тип `AuthUser`, добавить `displayName`, `firstName`, `lastName`, `gender`, `avatar`; убрать `name` +2. **`client/src/features/auth-oauth/`** — новая FSD-фича: + - `lib/oauth-providers.ts` — конфигурация: `{ id, label, icon, color }` для yandex и vk + - `ui/OAuthButtons.tsx` — компонент с двумя кнопками (Stack + Button variant="outlined"), каждая редиректит на `/api/auth/oauth/{provider}` + - `index.ts` — barrel экспорт +3. **`client/src/pages/auth/ui/AuthPage.tsx`** — добавить `` после формы email-кода, разделив Divider'ом с текстом «или» +4. **`client/src/**/*.tsx`** — найти и заменить все использования `user.name` → `user.displayName` + +### ENV + +Переменные в `server/.env` (из примера): + +``` +SERVER_PUBLIC_URL=http://127.0.0.1:3333 +CLIENT_PUBLIC_URL=http://127.0.0.1:5173 +YANDEX_CLIENT_ID=<значение> +YANDEX_CLIENT_SECRET=<значение> +VK_CLIENT_ID=<значение> +VK_CLIENT_SECRET=<значение> +``` + +Redirect URI для настройки в кабинетах провайдеров: +- Яндекс (локально): `http://127.0.0.1:3333/api/auth/oauth/yandex/callback` +- VK (локально): `http://127.0.0.1:3333/api/auth/oauth/vk/callback` +- Яндекс (прод): `https://любимыйкреатив.рф/api/auth/oauth/yandex/callback` +- VK (прод): `https://любимыйкреатив.рф/api/auth/oauth/vk/callback` + +## Структура фичи `features/auth-oauth/` + +``` +features/auth-oauth/ + index.ts — barrel: export { OAuthButtons } + ui/ + OAuthButtons.tsx — Stack из 2 кнопок (Яндекс, VK) + lib/ + oauth-providers.ts — массив провайдеров: { id, label, icon, color } +``` + +## Data flow (OAuth) + +``` +Клиент: кнопка «Войти через Яндекс/VK» + → редирект на /api/auth/oauth/{yandex|vk} +Сервер: формирует state JWT, редиректит на Яндекс/VK + → пользователь авторизуется у провайдера + → провайдер редиректит на /api/auth/oauth/{yandex|vk}/callback +Сервер: обменивает code на токен → получает профиль → findOrCreateUserFromOAuth() + → генерирует JWT → редиректит на {CLIENT_PUBLIC_URL}/auth/callback?token= +Клиент: AuthCallbackPage читает token → сохраняет в localStorage → редирект на / +``` + +## Не входит в scope + +- Отображение аватара в хедере/UserMenu (будет отдельно) +- Страница профиля с новыми полями (будет отдельно) +- OAuth для админа (админ только email/код) + +## Примечания + +- `gender` — nullable, если провайдер не вернул пол +- VK: `sex: 1` = female, `sex: 2` = male → нормализуем в `female` / `male` +- Яндекс: avatar — конструируем URL из `default_avatar_id`, поле `is_avatar_empty` подскажет, загружен ли аватар +- Яндекс scopes: `login:email login:info` +- VK scopes: `email` +- OAuth state — JWT с `expiresIn: 15m`