fix(server): remove all avatarType references after DB column drop
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,300 @@
|
|||||||
|
# Auth Redesign — Spec
|
||||||
|
|
||||||
|
**Date:** 2026-05-22
|
||||||
|
**Goal:** Переработать систему аутентификации: OAuth запрашивает только email, убрать внешние аватары, добавить вход по email+паролю, дать пользователям связывать методы входа в ЛК.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Data Model (Prisma)
|
||||||
|
|
||||||
|
### 1.1. Модель `User` — изменения
|
||||||
|
|
||||||
|
| Поле | Было | Стало |
|
||||||
|
|------|------|-------|
|
||||||
|
| `passwordHash` | `String?` (не использовалось) | Задействуем. Хранит bcrypt-хеш. `null` если пароль не установлен. |
|
||||||
|
| `avatarType` | `String?` (`'oauth'` / `'generated'`) | **Удалить.** Все аватары внутренние (DiceBear). |
|
||||||
|
| `avatar` | `String?` (URL или data:uri) | Только DiceBear URL или `null` (генерируется на лету) |
|
||||||
|
| `avatarStyle` | `String?` | Без изменений. |
|
||||||
|
|
||||||
|
Остальные поля (`id`, `email`, `displayName`, `firstName`, `lastName`, `gender`, `createdAt`, `updatedAt`) — без изменений. `firstName`, `lastName`, `gender` больше не заполняются при OAuth, но остаются в БД (могут быть заполнены пользователем вручную позже).
|
||||||
|
|
||||||
|
### 1.2. Модель `OAuthAccount` — без изменений
|
||||||
|
|
||||||
|
```prisma
|
||||||
|
model OAuthAccount {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
provider String // 'vk' | 'yandex'
|
||||||
|
providerUserId String
|
||||||
|
userId String
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
accessToken String?
|
||||||
|
refreshToken String? // зарезервировано, не используется сейчас
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
@@unique([provider, providerUserId])
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.3. Модель `AuthCode` — без изменений
|
||||||
|
|
||||||
|
### 1.4. Миграции
|
||||||
|
|
||||||
|
1. Удаление колонки `avatarType` из `User`
|
||||||
|
2. Миграция данных: для всех пользователей с `avatarType = 'oauth'` установить `avatar = null` (внешние URL больше не используются, аватар перегенерируется DiceBear)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Авторизация по email+паролю
|
||||||
|
|
||||||
|
### 2.1. Регистрация
|
||||||
|
|
||||||
|
`POST /api/auth/register` (новый, без аутентификации)
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"email": "user@example.com",
|
||||||
|
"password": "Abcdef1!",
|
||||||
|
"displayName": "Иван" // optional
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Валидация:**
|
||||||
|
- `email`: валидный email, нормализация (trim + lowercase), уникальность
|
||||||
|
- `password`: минимум 8 символов, минимум 1 буква, 1 цифра, 1 спецсимвол
|
||||||
|
- `displayName`: опционально, строка до 100 символов. Если не передан — берётся часть email до `@`
|
||||||
|
|
||||||
|
**Логика:**
|
||||||
|
1. Проверка, что email не занят → 409 если занят
|
||||||
|
2. `passwordHash = await bcrypt.hash(password, 10)`
|
||||||
|
3. Создание пользователя: `email`, `passwordHash`, `displayName`, `avatar = null`, `avatarStyle = 'avataaars'`
|
||||||
|
4. Создание `NotificationPreference` (как сейчас в verify-code)
|
||||||
|
5. Возврат JWT + user
|
||||||
|
|
||||||
|
**Response 201:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"token": "jwt...",
|
||||||
|
"user": { "id", "email", "displayName", "avatar", "avatarStyle", "isAdmin": false }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2. Вход
|
||||||
|
|
||||||
|
`POST /api/auth/login` (новый, без аутентификации)
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"email": "user@example.com",
|
||||||
|
"password": "Abcdef1!"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Rate limit:** максимум 5 попыток в минуту с одного IP (использовать `@fastify/rate-limit`). При превышении — `429 Too Many Requests`.
|
||||||
|
|
||||||
|
**Логика:**
|
||||||
|
1. Нормализация email
|
||||||
|
2. Поиск пользователя по email
|
||||||
|
3. Если пользователь не найден ИЛИ `passwordHash === null` → `401 Invalid email or password` (одинаковый ответ для безопасности)
|
||||||
|
4. `await bcrypt.compare(password, user.passwordHash)` → если не совпадает → `401`
|
||||||
|
5. Возврат JWT + user
|
||||||
|
|
||||||
|
### 2.3. Админ и пароль
|
||||||
|
|
||||||
|
- Админ (`email === ADMIN_EMAIL`) **не может** зарегистрироваться или войти по паролю
|
||||||
|
- `POST /api/auth/register` и `POST /api/auth/login` возвращают `403` для админского email
|
||||||
|
- Админ также **не может** установить пароль через `POST /api/me/password`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. OAuth (только email)
|
||||||
|
|
||||||
|
### 3.1. Scope
|
||||||
|
|
||||||
|
| Провайдер | Было | Стало |
|
||||||
|
|-----------|------|-------|
|
||||||
|
| VK | `email` | `email` (без изменений, но больше не запрашиваем профиль) |
|
||||||
|
| Яндекс | `login:email login:info` | `login:email` |
|
||||||
|
|
||||||
|
### 3.2. Callback — что убираем
|
||||||
|
|
||||||
|
**VK:**
|
||||||
|
- Больше не делаем `users.get` после получения токена
|
||||||
|
- Не сохраняем: `first_name`, `last_name`, `photo_200`, `sex`
|
||||||
|
|
||||||
|
**Яндекс:**
|
||||||
|
- Всё ещё вызываем `GET https://login.yandex.ru/info` (нужен для получения email)
|
||||||
|
- Из ответа берём только `default_email` или первый из `emails`
|
||||||
|
- **Не сохраняем:** `first_name`, `last_name`, `display_name`, `sex`, `default_avatar_id`
|
||||||
|
|
||||||
|
### 3.3. Callback — новая логика
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Обмен code на access_token (как сейчас)
|
||||||
|
2. Извлечение email из ответа провайдера:
|
||||||
|
- VK: поле `email` в ответе access_token
|
||||||
|
- Яндекс: вызываем `/info`, из ответа берём `default_email` или первый из `emails`
|
||||||
|
3. Если email отсутствует → редирект с ?oauthError=no_email
|
||||||
|
4. Нормализация email
|
||||||
|
5. Поиск пользователя по email:
|
||||||
|
a) Найден → привязываем OAuthAccount (если ещё не привязан), возвращаем JWT
|
||||||
|
b) Не найден → создаём нового:
|
||||||
|
- email
|
||||||
|
- displayName = часть email до @
|
||||||
|
- avatar = null
|
||||||
|
- avatarStyle = 'avataaars'
|
||||||
|
- Создаём OAuthAccount
|
||||||
|
- Создаём NotificationPreference
|
||||||
|
- Возвращаем JWT
|
||||||
|
6. Редирект на CLIENT_PUBLIC_URL/auth/callback?token=...
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fallback-email `{provider}_{id}@oauth.craftshop.local` — убираем.** Если провайдер не дал email — ошибка.
|
||||||
|
|
||||||
|
### 3.4. State-параметр
|
||||||
|
|
||||||
|
Без изменений. JWT с `expiresIn: 15m` для CSRF-защиты.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Связывание аккаунтов
|
||||||
|
|
||||||
|
### 4.1. Авто-связывание
|
||||||
|
|
||||||
|
При OAuth-входе: если email из OAuth совпадает с email существующего пользователя — автоматически создаётся `OAuthAccount`, связывающий провайдера с пользователем. Вход происходит мгновенно.
|
||||||
|
|
||||||
|
### 4.2. Ручное связывание — страница настроек `/me`
|
||||||
|
|
||||||
|
**Получение статуса методов:** `GET /api/me/auth-methods` (новый, требует `authenticate`)
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"methods": [
|
||||||
|
{ "type": "password", "active": true },
|
||||||
|
{ "type": "vk", "active": false },
|
||||||
|
{ "type": "yandex", "active": true }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Логика:
|
||||||
|
- `type: "password"` — `active: user.passwordHash !== null`
|
||||||
|
- `type: "vk"` / `type: "yandex"` — `active: exists(OAuthAccount)`
|
||||||
|
|
||||||
|
### 4.3. Привязка OAuth
|
||||||
|
|
||||||
|
`GET /api/auth/oauth/{provider}/link` (новый, требует `authenticate`)
|
||||||
|
|
||||||
|
1. Генерирует state-JWT с `{ userId, provider, action: 'link' }`, `expiresIn: 15m`
|
||||||
|
2. Редиректит на страницу авторизации провайдера
|
||||||
|
3. После callback проверяет `action: 'link'` в state
|
||||||
|
4. Создаёт `OAuthAccount` для указанного `userId` (нормальный upsert)
|
||||||
|
5. Редиректит на `/me?linked={provider}`
|
||||||
|
|
||||||
|
### 4.4. Привязка пароля
|
||||||
|
|
||||||
|
`POST /api/me/password` (новый, требует `authenticate`)
|
||||||
|
|
||||||
|
**Request:** `{ "password": "Abcdef1!" }`
|
||||||
|
|
||||||
|
**Логика:**
|
||||||
|
1. Если `user.email === ADMIN_EMAIL` → `403`
|
||||||
|
2. Если `user.passwordHash !== null` → `409 Password already set` (для смены использовать отдельный метод)
|
||||||
|
3. Валидация пароля (8+, буква+цифра+спецсимвол)
|
||||||
|
4. `user.passwordHash = await bcrypt.hash(password, 10)`
|
||||||
|
5. Сохранение пользователя
|
||||||
|
|
||||||
|
### 4.5. Отвязывание
|
||||||
|
|
||||||
|
`DELETE /api/me/oauth/{provider}` (новый, требует `authenticate`)
|
||||||
|
|
||||||
|
**Логика:**
|
||||||
|
1. Удаление `OAuthAccount` по `userId + provider`
|
||||||
|
2. Если `OAuthAccount` не найден → `404`
|
||||||
|
3. **Проверка последнего метода:** после удаления, если у пользователя нет ни `passwordHash`, ни других `OAuthAccount` → `400 Cannot remove last auth method`
|
||||||
|
4. Возврат `200`
|
||||||
|
|
||||||
|
**Примечание:** отвязывание пароля (установка `passwordHash = null`) пока не делаем — можно добавить позже.
|
||||||
|
|
||||||
|
### 4.6. Админ и связывание
|
||||||
|
|
||||||
|
- `POST /api/me/password` → `403` для админа
|
||||||
|
- OAuth-привязка через `/link` → `403` для админа
|
||||||
|
- OAuth-отвязывание → `403` для админа
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Email-код (без изменений логики)
|
||||||
|
|
||||||
|
`POST /api/auth/request-code` и `POST /api/auth/verify-code` работают как раньше, изменений нет. Админ входит только этим способом.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Изменения на клиенте
|
||||||
|
|
||||||
|
### 6.1. Страница `/auth`
|
||||||
|
|
||||||
|
**3 вкладки:**
|
||||||
|
- **«Пароль»** — переключатель Вход/Регистрация. Вход: email + пароль. Регистрация: email + пароль + подтверждение пароля + имя (опционально).
|
||||||
|
- **«Код»** — как сейчас: email → отправить код → ввести код.
|
||||||
|
- **«Другой способ»** — кнопки Войти через VK / Яндекс.
|
||||||
|
|
||||||
|
### 6.2. Страница `/me` (настройки)
|
||||||
|
|
||||||
|
Новая секция «Методы входа»:
|
||||||
|
- Список методов с индикаторами «привязан» / «не привязан»
|
||||||
|
- Кнопки «Привязать» (редирект на OAuth или форма пароля)
|
||||||
|
- Кнопки «Отвязать» (disabled если это последний метод)
|
||||||
|
- Для админа — секция скрыта
|
||||||
|
|
||||||
|
### 6.3. Effector-стейт
|
||||||
|
|
||||||
|
- `$token`, `$user`, `tokenSet`, `logout` — без изменений
|
||||||
|
- Добавить эффекты: `loginFx`, `registerFx`, `linkOAuthFx`, `setPasswordFx`, `unlinkOAuthFx`
|
||||||
|
|
||||||
|
### 6.4. Компоненты
|
||||||
|
|
||||||
|
- `UserAvatar` — убрать проверку `avatarType`, всегда использовать DiceBear (сохранённый `avatar` или генерация на лету)
|
||||||
|
- `OAuthButtons` — без изменений (URL те же)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Тестирование
|
||||||
|
|
||||||
|
### 7.1. Серверные тесты
|
||||||
|
|
||||||
|
- `POST /api/auth/register` — успешная регистрация, дубликат email, слабый пароль
|
||||||
|
- `POST /api/auth/login` — успешный вход, неверный пароль, несуществующий email, превышение rate limit
|
||||||
|
- OAuth callback — создание нового пользователя с email, авто-связывание по email, ошибка при отсутствии email от провайдера
|
||||||
|
- `POST /api/me/password` — установка, повторная установка (409), админ (403)
|
||||||
|
- `GET /api/me/auth-methods` — корректный список методов
|
||||||
|
- `DELETE /api/me/oauth/{provider}` — отвязывание, последний метод (400), админ (403)
|
||||||
|
- Админ не может войти через `/login` (403)
|
||||||
|
|
||||||
|
### 7.2. Клиентские тесты
|
||||||
|
|
||||||
|
- Страница `/auth` — наличие трёх вкладок, переключение
|
||||||
|
- Форма регистрации — валидация пароля, подтверждение
|
||||||
|
- Форма входа — обработка ошибок
|
||||||
|
- `/me` — отображение методов, кнопки привязки/отвязки
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Миграция существующих пользователей
|
||||||
|
|
||||||
|
1. Все пользователи с `avatarType = 'oauth'`: `avatar = null`, `avatarType = null`. Аватар перегенерируется DiceBear при следующем отображении.
|
||||||
|
2. `avatarType` колонка удаляется из БД.
|
||||||
|
3. Существующие OAuth-аккаунты работают как раньше, но при следующем входе через OAuth обновляется логика (не запрашиваем профиль).
|
||||||
|
4. `firstName`, `lastName`, `gender` у существующих OAuth-пользователей остаются в БД (не удаляем, просто больше не пополняем из OAuth).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Заметки
|
||||||
|
|
||||||
|
- **bcrypt** — не установлен, нужно добавить `npm install bcrypt` в server.
|
||||||
|
- **Rate limit** — `@fastify/rate-limit` не установлен. Добавить или реализовать самодельный in-memory rate limiter (5 попыток/мин/IP).
|
||||||
|
- Все новые эндпоинты должны валидироваться через JSON Schema (как существующие).
|
||||||
|
- Пароль никогда не возвращается в ответах API и не логируется.
|
||||||
|
- Существующий `User.passwordHash` (null у всех) — колонка уже есть, миграция БД не нужна, просто начинаем использовать.
|
||||||
Generated
+35
@@ -13,6 +13,7 @@
|
|||||||
"@fastify/multipart": "^10.0.0",
|
"@fastify/multipart": "^10.0.0",
|
||||||
"@fastify/static": "^9.1.3",
|
"@fastify/static": "^9.1.3",
|
||||||
"@prisma/client": "5.22.0",
|
"@prisma/client": "5.22.0",
|
||||||
|
"bcrypt": "^6.0.0",
|
||||||
"dotenv": "^17.4.2",
|
"dotenv": "^17.4.2",
|
||||||
"fastify": "^5.8.5",
|
"fastify": "^5.8.5",
|
||||||
"nodemailer": "^8.0.7",
|
"nodemailer": "^8.0.7",
|
||||||
@@ -2130,6 +2131,29 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/bcrypt": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"node-addon-api": "^8.3.0",
|
||||||
|
"node-gyp-build": "^4.8.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/bcrypt/node_modules/node-addon-api": {
|
||||||
|
"version": "8.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.7.0.tgz",
|
||||||
|
"integrity": "sha512-9MdFxmkKaOYVTV+XVRG8ArDwwQ77XIgIPyKASB1k3JPq3M8fGQQQE3YpMOrKm6g//Ktx8ivZr8xo1Qmtqub+GA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "^18 || ^20 || >= 21"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/bl": {
|
"node_modules/bl": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
|
||||||
@@ -3605,6 +3629,17 @@
|
|||||||
"integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==",
|
"integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/node-gyp-build": {
|
||||||
|
"version": "4.8.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
|
||||||
|
"integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"node-gyp-build": "bin.js",
|
||||||
|
"node-gyp-build-optional": "optional.js",
|
||||||
|
"node-gyp-build-test": "build-test.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/nodemailer": {
|
"node_modules/nodemailer": {
|
||||||
"version": "8.0.7",
|
"version": "8.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.7.tgz",
|
||||||
|
|||||||
Binary file not shown.
@@ -3,6 +3,13 @@ const windows = new Map()
|
|||||||
const MAX_ATTEMPTS = 5
|
const MAX_ATTEMPTS = 5
|
||||||
const WINDOW_MS = 60_000
|
const WINDOW_MS = 60_000
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
const now = Date.now()
|
||||||
|
for (const [ip, entry] of windows) {
|
||||||
|
if (now - entry.start > WINDOW_MS) windows.delete(ip)
|
||||||
|
}
|
||||||
|
}, 5 * 60_000).unref()
|
||||||
|
|
||||||
export function checkLoginRateLimit(ip) {
|
export function checkLoginRateLimit(ip) {
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
const entry = windows.get(ip)
|
const entry = windows.get(ip)
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ export async function registerAdminProfileRoutes(fastify) {
|
|||||||
email: user.email,
|
email: user.email,
|
||||||
displayName: user.displayName,
|
displayName: user.displayName,
|
||||||
avatar: user.avatar,
|
avatar: user.avatar,
|
||||||
avatarType: user.avatarType,
|
|
||||||
avatarStyle: user.avatarStyle,
|
avatarStyle: user.avatarStyle,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -25,7 +24,6 @@ export async function registerAdminProfileRoutes(fastify) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
avatar: user.avatar,
|
avatar: user.avatar,
|
||||||
avatarType: user.avatarType,
|
|
||||||
avatarStyle: user.avatarStyle,
|
avatarStyle: user.avatarStyle,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -37,17 +35,12 @@ export async function registerAdminProfileRoutes(fastify) {
|
|||||||
nameRaw === undefined ? undefined : nameRaw === null ? null : nameRaw === '' ? null : String(nameRaw).trim()
|
nameRaw === undefined ? undefined : nameRaw === null ? null : nameRaw === '' ? null : String(nameRaw).trim()
|
||||||
const avatarRaw = request.body?.avatar
|
const avatarRaw = request.body?.avatar
|
||||||
const avatar = avatarRaw === null || avatarRaw === undefined ? undefined : String(avatarRaw).trim()
|
const avatar = avatarRaw === null || avatarRaw === undefined ? undefined : String(avatarRaw).trim()
|
||||||
const avatarTypeRaw = request.body?.avatarType
|
|
||||||
const avatarType = avatarTypeRaw === null || avatarTypeRaw === undefined ? undefined : String(avatarTypeRaw).trim()
|
|
||||||
const avatarStyleRaw = request.body?.avatarStyle
|
const avatarStyleRaw = request.body?.avatarStyle
|
||||||
const avatarStyle =
|
const avatarStyle =
|
||||||
avatarStyleRaw === null || avatarStyleRaw === undefined ? undefined : String(avatarStyleRaw).trim()
|
avatarStyleRaw === null || avatarStyleRaw === undefined ? undefined : String(avatarStyleRaw).trim()
|
||||||
|
|
||||||
if (displayName !== undefined && displayName !== null && displayName.length > 40)
|
if (displayName !== undefined && displayName !== null && displayName.length > 40)
|
||||||
return reply.code(400).send({ error: 'Имя/ник максимум 40 символов' })
|
return reply.code(400).send({ error: 'Имя/ник максимум 40 символов' })
|
||||||
if (avatarType !== undefined && avatarType !== 'oauth' && avatarType !== 'generated' && avatarType !== '') {
|
|
||||||
return reply.code(400).send({ error: 'avatarType должен быть oauth | generated | пустая строка' })
|
|
||||||
}
|
|
||||||
if (avatar !== undefined && avatar.length > 200000) return reply.code(400).send({ error: 'Аватар слишком большой' })
|
if (avatar !== undefined && avatar.length > 200000) return reply.code(400).send({ error: 'Аватар слишком большой' })
|
||||||
if (avatarStyle !== undefined && avatarStyle !== '' && avatarStyle.length > 30) {
|
if (avatarStyle !== undefined && avatarStyle !== '' && avatarStyle.length > 30) {
|
||||||
return reply.code(400).send({ error: 'Стиль аватара слишком длинный' })
|
return reply.code(400).send({ error: 'Стиль аватара слишком длинный' })
|
||||||
@@ -57,9 +50,6 @@ export async function registerAdminProfileRoutes(fastify) {
|
|||||||
if (displayName !== undefined) {
|
if (displayName !== undefined) {
|
||||||
data.displayName = displayName && displayName.length ? displayName : null
|
data.displayName = displayName && displayName.length ? displayName : null
|
||||||
}
|
}
|
||||||
if (avatarType !== undefined) {
|
|
||||||
data.avatarType = avatarType === '' ? null : avatarType
|
|
||||||
}
|
|
||||||
if (avatar !== undefined) {
|
if (avatar !== undefined) {
|
||||||
data.avatar = avatar === '' ? null : avatar
|
data.avatar = avatar === '' ? null : avatar
|
||||||
}
|
}
|
||||||
@@ -73,7 +63,6 @@ export async function registerAdminProfileRoutes(fastify) {
|
|||||||
email: updated.email,
|
email: updated.email,
|
||||||
displayName: updated.displayName,
|
displayName: updated.displayName,
|
||||||
avatar: updated.avatar,
|
avatar: updated.avatar,
|
||||||
avatarType: updated.avatarType,
|
|
||||||
avatarStyle: updated.avatarStyle,
|
avatarStyle: updated.avatarStyle,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ function mapUserForClient(user) {
|
|||||||
lastName: user.lastName,
|
lastName: user.lastName,
|
||||||
gender: user.gender,
|
gender: user.gender,
|
||||||
avatar: user.avatar,
|
avatar: user.avatar,
|
||||||
avatarType: user.avatarType,
|
|
||||||
avatarStyle: user.avatarStyle,
|
avatarStyle: user.avatarStyle,
|
||||||
isAdmin: Boolean(adminEmail) && userEmail === adminEmail,
|
isAdmin: Boolean(adminEmail) && userEmail === adminEmail,
|
||||||
}
|
}
|
||||||
@@ -197,17 +196,12 @@ export async function registerAuthRoutes(fastify) {
|
|||||||
const displayName = nameRaw === null || nameRaw === undefined ? null : String(nameRaw).trim()
|
const displayName = nameRaw === null || nameRaw === undefined ? null : String(nameRaw).trim()
|
||||||
const avatarRaw = request.body?.avatar
|
const avatarRaw = request.body?.avatar
|
||||||
const avatar = avatarRaw === null || avatarRaw === undefined ? undefined : String(avatarRaw).trim()
|
const avatar = avatarRaw === null || avatarRaw === undefined ? undefined : String(avatarRaw).trim()
|
||||||
const avatarTypeRaw = request.body?.avatarType
|
|
||||||
const avatarType = avatarTypeRaw === null || avatarTypeRaw === undefined ? undefined : String(avatarTypeRaw).trim()
|
|
||||||
const avatarStyleRaw = request.body?.avatarStyle
|
const avatarStyleRaw = request.body?.avatarStyle
|
||||||
const avatarStyle =
|
const avatarStyle =
|
||||||
avatarStyleRaw === null || avatarStyleRaw === undefined ? undefined : String(avatarStyleRaw).trim()
|
avatarStyleRaw === null || avatarStyleRaw === undefined ? undefined : String(avatarStyleRaw).trim()
|
||||||
|
|
||||||
if (displayName !== null && displayName.length > 40)
|
if (displayName !== null && displayName.length > 40)
|
||||||
return reply.code(400).send({ error: 'Имя/ник максимум 40 символов' })
|
return reply.code(400).send({ error: 'Имя/ник максимум 40 символов' })
|
||||||
if (avatarType !== undefined && avatarType !== 'oauth' && avatarType !== 'generated' && avatarType !== '') {
|
|
||||||
return reply.code(400).send({ error: 'avatarType должен быть oauth | generated | пустая строка' })
|
|
||||||
}
|
|
||||||
if (avatar !== undefined && avatar.length > 200000) return reply.code(400).send({ error: 'Аватар слишком большой' })
|
if (avatar !== undefined && avatar.length > 200000) return reply.code(400).send({ error: 'Аватар слишком большой' })
|
||||||
if (avatarStyle !== undefined && avatarStyle !== '' && avatarStyle.length > 30) {
|
if (avatarStyle !== undefined && avatarStyle !== '' && avatarStyle.length > 30) {
|
||||||
return reply.code(400).send({ error: 'Стиль аватара слишком длинный' })
|
return reply.code(400).send({ error: 'Стиль аватара слишком длинный' })
|
||||||
@@ -217,9 +211,6 @@ export async function registerAuthRoutes(fastify) {
|
|||||||
displayName: displayName && displayName.length ? displayName : null,
|
displayName: displayName && displayName.length ? displayName : null,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (avatarType !== undefined) {
|
|
||||||
data.avatarType = avatarType === '' ? null : avatarType
|
|
||||||
}
|
|
||||||
if (avatar !== undefined) {
|
if (avatar !== undefined) {
|
||||||
data.avatar = avatar === '' ? null : avatar
|
data.avatar = avatar === '' ? null : avatar
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user