Перейти к основному содержимому

Пользователи

Канонический DDL находится в ../database-schema.md. SQL ниже является концептуальным примером feature-модели и не используется для миграций.

Зачем нужно

Документ описывает модель пользователей, профилей, контактов и связи пользователя с identity auth subsystemом.

1. User entity

1.1. Таблица users

CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

-- Персональные данные
first_name VARCHAR(255) NOT NULL,
last_name VARCHAR(255) NOT NULL,
patronymic VARCHAR(255),
username VARCHAR(100) UNIQUE, -- публичный slug для GET /users/:idOrSlug
date_of_birth DATE,
gender VARCHAR(20),
avatar_url VARCHAR(500), -- см. примечание об аватарах ниже

-- Предпочтения
locale VARCHAR(10) DEFAULT 'ru', -- предпочитаемый язык
timezone VARCHAR(50), -- часовой пояс (IANA)

-- Состояние
is_banned BOOLEAN NOT NULL DEFAULT FALSE,
banned_at TIMESTAMPTZ,

-- Метаданные
last_active_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

1.2. Принципы

  • Пароль в отдельной таблицеusers не содержит password_hash. Управление паролями делегировано identity auth subsystemу через таблицу user_credentials (см. раздел 4).
  • Нет удаления — пользователь может быть забанен (is_banned), но не удалён. Soft delete не используется.
  • Data erasure (GDPR) — при запросе на удаление персональных данных выполняется anonymization:
    • first_name → "Удалённый", last_name → "пользователь"
    • username → null, date_of_birth → null, gender → null, avatar_url → null (файл удаляется)
    • patronymic → null, timezone → null
    • Все записи user_emails и user_phones удаляются
    • is_banned → true (аккаунт блокируется)
    • user_credentials.password_hash → null (вход невозможен)
    • Audit logs и event logs сохраняются (userId → anonymized user, нет PII)
    • Операция необратима, выполняется только администратором по запросу пользователя
  • Банis_banned: true + banned_at: timestamp. Забаненный пользователь не может войти. Админ может разбанить.
  • last_active_at — обновляется при значимых действиях (вход, refresh token). Debounce: обновлять не чаще раза в 5 минут (проверка last_active_at < NOW() - interval '5 min' перед UPDATE), чтобы не генерировать UPDATE на каждый refresh (каждые 15 мин).
  • username — публичный slug для GET /users/:idOrSlug. Уникален, опционален.
    • Длина: 3–30 символов
    • Разрешённые символы: a-z, 0-9, -, _ (только lowercase)
    • Не может начинаться/заканчиваться на - или _
    • Case insensitive: хранится в lowercase, поиск регистронезависимый
    • Reserved words: admin, api, auth, system, root, superadmin, null, undefined, settings, security, profile, login, register, logout — запрещены при создании/обновлении
  • Аватар:
    • Загрузка: POST /users/:id/avatar (multipart/form-data)
    • Хранение: локальная ФС или S3 (настраивается через env STORAGE_DRIVER)
    • Ограничения: макс. 2MB, форматы jpg/png/webp
    • Обработка: resize до 256×256 px, crop по центру, конвертация в webp
    • URL: /uploads/avatars/{userId}.webp (локальная ФС) или S3 presigned URL
    • Удаление: DELETE /users/:id/avatar → avatar_url = null, файл удаляется
    • Дефолт: если avatar_url = null, фронтенд показывает инициалы (первые буквы first_name + last_name) на цветном фоне (цвет генерируется из userId hash)

2. Контакты

2.1. Email'ы (user_emails)

У пользователя может быть несколько email. Один — primary.

CREATE TABLE user_emails (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id),
email VARCHAR(255) NOT NULL UNIQUE,
is_primary BOOLEAN NOT NULL DEFAULT FALSE,
is_verified BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

2.2. Телефоны (user_phones)

Аналогичная структура.

CREATE TABLE user_phones (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id),
phone VARCHAR(30) NOT NULL UNIQUE,
is_primary BOOLEAN NOT NULL DEFAULT FALSE,
is_verified BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

2.3. Правила

  • Всегда ровно один primary email и один primary phone (если есть хотя бы один)
  • Смена primary — транзакционная операция (снять со всех → поставить новому)
  • Нельзя удалить последний email/phoneContactService проверяет перед удалением и выбрасывает ошибку, если запись единственная
  • Верификация — через код (VerificationCodeService из identity auth subsystemа)

3. Кастомные поля

Произвольные поля, которые создаёт админ. Заполняются через UI, API, или при регистрации (шаг воронки).

3.1. Определения полей (user_field_definitions)

CREATE TABLE user_field_definitions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL UNIQUE,
alias VARCHAR(255),
type VARCHAR(30) NOT NULL, -- text, number, boolean, date, select, json
default_value JSONB,
required BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

3.2. Значения полей (user_field_values)

CREATE TABLE user_field_values (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
field_id UUID NOT NULL REFERENCES user_field_definitions(id),
user_id UUID NOT NULL REFERENCES users(id),
value JSONB NOT NULL,
UNIQUE(field_id, user_id)
);

3.3. Типы полей

ТипЗначение
textСтрока
numberЧисло
booleanДа/нет
dateДата
selectВыбор из вариантов; варианты хранятся в default_value определения в формате { "options": ["option1", "option2", ...] }
jsonПроизвольный JSON

4. Связь с identity auth subsystemом

4.1. Что хранится в auth, а не в users

ДанныеГдеПочему
Пароль пользователяuser_credentials (auth) — отдельная таблицаУправление паролями принадлежит identity auth subsystemу
Методы входа пользователяuser_auth_methods (auth)Это конфигурация аутентификации
TOTP secretuser_auth_methods.settings (auth)Часть метода входа
OAuth привязкиuser_auth_methods (auth)Часть аутентификации
MFA настройкиuser_auth_methods (auth)Часть воронки

4.2. user_credentials (identity auth subsystem)

Отдельная таблица — не JSONB-поле в users. Хранит хешированный пароль, привязанный к пользователю.

CREATE TABLE user_credentials (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL UNIQUE REFERENCES users(id),
password_hash VARCHAR(255), -- bcrypt; nullable: при анонимизации устанавливается в NULL
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

4.3. user_auth_methods (identity auth subsystem)

Canonical DDL is in ../database-schema.md. Conceptual fields: type, provider, external_subject, settings, is_enabled, is_verified, last_used_at.

При сборке воронки входа:

  1. Обязательные шаги (mandatory: true) — всегда
  2. Необязательные шаги (mandatory: false) — проверяем user_auth_methods.is_enabled
  3. Дополнительные разрешённые методы — если is_enabled: true → добавляем
  4. Ограничение: хотя бы один метод входа должен быть активен
  5. При регистрации обязателен хотя бы один verified authentication method; password credential требуется только если local password login включён для пользователя.
  6. Пароль при входе — обычный шаг с mandatory: true/false

5. Что вынесено из users

ПодсистемаКудаПричина
Парольuser_credentials (auth)Управление паролями принадлежит identity auth subsystemу
External identitiesauthЧасть аутентификации
2FA/TOTP settingsauth (user_auth_methods)Часть воронки
Роли, назначенияrbacОтдельный домен
Membership в org/teamorganizationsОтдельный домен
Device accountssaved_accounts + device_bindingsБыстрый повторный вход без хранения токенов на клиенте
РефералыРеферальная система будет описана в отдельном документе

6. API-эндпоинты

Пользователи (admin)

HTTPПутьAuthНазначение
GET/api/v2/identity/admin/usersJWT + identity.users.readСписок пользователей (см. фильтры ниже)
GET/api/v2/identity/admin/users/:idOrSlugJWT + identity.users.readДетали пользователя
POST/api/v2/identity/admin/usersJWT + identity.users.manageСоздать пользователя
PATCH/api/v2/identity/admin/users/:idJWT + identity.users.manageРедактировать пользователя
POST/api/v2/identity/admin/users/:id/banJWT + identity.users.blockЗабанить
POST/api/v2/identity/admin/users/:id/unbanJWT + identity.users.blockРазбанить
GET/api/v2/identity/admin/users/:id/rolesJWT + identity.users.readРоли пользователя
POST/api/v2/identity/admin/users/:id/anonymizeJWT + identity.users.deleteAnonymization (GDPR data erasure)

GET /users — фильтры и поиск:

ПараметрТипОписание
searchstringПоиск по first_name, last_name, email, phone, username (ILIKE %query%)
is_bannedbooleanФильтр по статусу бана
rolestringФильтр по роли (slug)
created_from / created_todateФильтр по дате регистрации
sortstringСортировка: created_at, last_active_at, first_name (default: created_at)
orderstringasc / desc (default: desc)
page / limitnumberПагинация (default: page=1, limit=20, max limit=100)

Профиль (текущий пользователь)

HTTPПутьAuthНазначение
GET/profileJWTМой профиль
PATCH/profileJWTОбновить профиль
POST/profile/avatarJWTЗагрузить аватар
DELETE/profile/avatarJWTУдалить аватар

Контакты

HTTPПутьAuthНазначение
GET/api/v2/identity/admin/users/:id/emailsJWTEmail'ы пользователя
POST/api/v2/identity/admin/users/:id/emailsJWTДобавить email
DELETE/api/v2/identity/admin/users/:id/emails/:emailIdJWTУдалить email
PATCH/api/v2/identity/admin/users/:id/emails/:emailId/primaryJWTСделать primary
GET/api/v2/identity/admin/users/:id/phonesJWTТелефоны пользователя
POST/api/v2/identity/admin/users/:id/phonesJWTДобавить телефон
DELETE/api/v2/identity/admin/users/:id/phones/:phoneIdJWTУдалить телефон
PATCH/api/v2/identity/admin/users/:id/phones/:phoneId/primaryJWTСделать primary

Кастомные поля

HTTPПутьAuthНазначение
GET/admin/custom-fieldsJWT + adminОпределения полей
POST/admin/custom-fieldsJWT + adminСоздать определение
PATCH/admin/custom-fields/:idJWT + adminРедактировать определение
DELETE/admin/custom-fields/:idJWT + adminУдалить определение
GET/api/v2/identity/admin/users/:id/custom-fieldsJWTЗначения полей пользователя
PATCH/api/v2/identity/admin/users/:id/custom-fieldsJWTОбновить значения

7. UX/UI

7.1. Dashboard — ProfileCard

ProfileCard отображается как первый блок на дашборде (maxContentWidth: 520px):

  • Аватар (xl) + имя + логин
  • Контакты: email и/или телефон
  • Баланс / игровые очки (если подключены)
  • Кнопка "Редактировать" → EditProfileModal
  • Нажатие на аватар → EditAvatarModal

EditProfileModal:

  • Поля: имя, фамилия, кастомные поля (определённые администратором)
  • Кнопки: "Сохранить" / "Отменить"

EditAvatarModal:

  • Загрузка изображения (drag & drop или выбор файла)
  • Превью с обрезкой
  • Альтернатива: выбор цветного фона (из avatarPalette / avatarGradients)

7.2. DataPage — Личные данные

Страница /:lang/data внутри DashboardLayout (maxContentWidth: 520px). Блоки:

Блок profile-card:

  • Аватар + имя + кастомные поля
  • Inline-редактирование каждого поля

Блок contacts:

  • Список email-адресов: каждый с бейджем "основной" / "подтверждён" / "не подтверждён"
  • Список телефонов: аналогично
  • Кнопка "+" для добавления → EditEmailModal / EditPhoneModal
  • Редактирование: нажатие на строку → модалка с полем ввода + верификация кодом

Блок external-accounts:

  • Подключённые OAuth-провайдеры (GitHub, Telegram и др.)
  • Для каждого: иконка провайдера + имя аккаунта + кнопка "Отключить"
  • Кнопка "Подключить" для добавления нового провайдера

Блок danger-zone:

  • Красная рамка, иконка предупреждения
  • Кнопка "Удалить профиль" → DeleteProfileModal (CriticalConfirmModal: красный акцент, повторный ввод подтверждения)

7.3. Состояния

  • Loading: скелетон (3 анимированных строки на каждый блок)
  • Error: ErrorState с кнопкой "Повторить"
  • Empty: EmptyState с иконкой и текстом
  • Успех редактирования: toast "Данные сохранены"
  • Ошибка валидации: красный текст под полем