- Auth: register без токенов до верификации (userId, message, verificationCode)
- Auth: login — 429 RATE_LIMIT_EXCEEDED при lockout, user с avatarUrl
- Auth: verify-email — { userId, code }, без Bearer
- Auth: reset-password — поле newPassword
- Profile: stats — byStack, totalTestsTaken, totalQuestions, correctAnswers, accuracy
- Tests: POST /tests возвращает полный список questions
- Tests: answer — полный snapshot отвеченного вопроса
- Tests: history — offset-пагинация (limit/offset), формат { tests, total }
- Admin: GET /admin/questions/pending, POST approve/reject, PATCH для редактирования
- DB: email_verification_codes, password_reset_tokens; обновлена question_cache_meta
- Security: CORS_ORIGINS из env, CSP/COEP отключены
- LLM: LLM_FALLBACK_MODEL, LLM_RETRY_DELAY_MS
- Onboarding: правило .env.example, JWT_SECRET >= 32 символов
11 KiB
Схема базы данных
Описание таблиц и связей. Фактическая Drizzle-схема создаётся в samreshu-backend, здесь — справочник.
Диаграмма связей
erDiagram
users ||--o{ subscriptions : has
users ||--o{ sessions : has
users ||--o{ email_verification_codes : has
users ||--o{ password_reset_tokens : has
users ||--o{ oauth_accounts : has
users ||--o| totp_secrets : has
users ||--o{ tests : takes
users ||--o{ user_stats : has
users ||--o{ user_achievements : earns
users ||--o{ user_question_log : tracks
tests ||--o{ test_questions : contains
question_bank ||--o{ question_cache_meta : has
question_bank ||--o{ question_reports : receives
Таблицы
users
Основная таблица пользователей.
| Колонка | Тип | Описание |
|---|---|---|
| id | uuid, PK | |
| varchar, unique | ||
| password_hash | varchar | bcrypt/argon2 |
| nickname | varchar | Отображаемое имя |
| avatar_url | varchar, nullable | |
| country | varchar, nullable | |
| city | varchar, nullable | |
| self_level | enum, nullable | jun / mid / sen |
| is_public | boolean, default true | Публичный профиль |
| role | enum, default 'free' | guest / free / pro / admin |
| email_verified_at | timestamptz, nullable | |
| created_at | timestamptz | |
| updated_at | timestamptz |
subscriptions
Подписки пользователей. Существует с первого дня (даже для Free — с plan='free').
| Колонка | Тип | Описание |
|---|---|---|
| id | uuid, PK | |
| user_id | uuid, FK → users | |
| plan | enum | free / pro |
| status | enum | active / trialing / cancelled / expired |
| started_at | timestamptz | |
| expires_at | timestamptz, nullable | |
| cancelled_at | timestamptz, nullable | |
| payment_provider | varchar, nullable | yukassa / cloudpayments |
| external_id | varchar, nullable | ID подписки у провайдера |
sessions
Активные сессии пользователя (устройства).
| Колонка | Тип | Описание |
|---|---|---|
| id | uuid, PK | |
| user_id | uuid, FK → users | |
| refresh_token_hash | varchar | |
| user_agent | varchar | |
| ip_address | varchar | |
| last_active_at | timestamptz | |
| expires_at | timestamptz | |
| created_at | timestamptz |
email_verification_codes
Коды подтверждения email (для регистрации).
| Колонка | Тип | Описание |
|---|---|---|
| id | uuid, PK | |
| user_id | uuid, FK → users | |
| code | varchar | Код из письма (обычно 6 цифр) |
| expires_at | timestamptz | Срок действия кода |
| created_at | timestamptz |
password_reset_tokens
Токены для сброса пароля.
| Колонка | Тип | Описание |
|---|---|---|
| id | uuid, PK | |
| user_id | uuid, FK → users | |
| token_hash | varchar | Хеш токена из письма |
| expires_at | timestamptz | Срок действия |
| created_at | timestamptz |
oauth_accounts
Привязанные OAuth-провайдеры (Phase 2).
| Колонка | Тип | Описание |
|---|---|---|
| id | uuid, PK | |
| user_id | uuid, FK → users | |
| provider | enum | github / google |
| provider_user_id | varchar | |
| created_at | timestamptz |
totp_secrets
2FA через TOTP (Phase 2).
| Колонка | Тип | Описание |
|---|---|---|
| id | uuid, PK | |
| user_id | uuid, FK → users, unique | |
| secret | varchar | Зашифрованный TOTP-секрет |
| enabled | boolean, default false | |
| created_at | timestamptz |
tests
Пройденные тесты.
| Колонка | Тип | Описание |
|---|---|---|
| id | uuid, PK | |
| user_id | uuid, FK → users | |
| stack | enum | html / css / js / ts / react / vue / nodejs / git / web_basics |
| level | enum | basic / beginner / intermediate / advanced / expert |
| question_count | integer | 10 / 20 / 30 |
| mode | enum | fixed / infinite / marathon |
| status | enum | in_progress / completed / abandoned |
| score | integer, nullable | Количество правильных |
| started_at | timestamptz | |
| finished_at | timestamptz, nullable | |
| time_limit_seconds | integer, nullable |
test_questions
Снепшот вопросов для конкретного теста. При старте теста вопросы копируются сюда из question_bank или генерируются LLM.
| Колонка | Тип | Описание |
|---|---|---|
| id | uuid, PK | |
| test_id | uuid, FK → tests | |
| question_bank_id | uuid, FK → question_bank, nullable | Если из банка |
| order_number | integer | Порядок в тесте |
| type | enum | single_choice / multiple_select / true_false / short_text |
| question_text | text | |
| options | jsonb, nullable | Варианты ответов |
| correct_answer | jsonb | Правильный ответ |
| explanation | text | Объяснение |
| user_answer | jsonb, nullable | Ответ пользователя |
| is_correct | boolean, nullable | |
| answered_at | timestamptz, nullable |
question_bank
Провалидированные вопросы для переиспользования.
| Колонка | Тип | Описание |
|---|---|---|
| id | uuid, PK | |
| stack | enum | |
| level | enum | |
| type | enum | |
| question_text | text | |
| options | jsonb, nullable | |
| correct_answer | jsonb | |
| explanation | text | |
| status | enum | pending / approved / rejected |
| source | enum | llm_generated / manual |
| usage_count | integer, default 0 | Сколько раз использован |
| created_at | timestamptz | |
| approved_at | timestamptz, nullable |
question_cache_meta
Метаданные генерации вопросов через LLM.
| Колонка | Тип | Описание |
|---|---|---|
| id | uuid, PK | |
| question_bank_id | uuid, FK → question_bank | |
| llm_model | varchar | Модель, сгенерировавшая вопрос |
| prompt_hash | varchar | Хеш промпта |
| generation_time_ms | integer | Время генерации в мс |
| valid | boolean | Прошёл ли валидацию с первого раза |
| retry_count | integer | Количество retry при ошибках |
| questions_generated | integer | Сколько вопросов сгенерировано |
| raw_response | text, nullable | Сырой ответ LLM (опционально, для отладки) |
| created_at | timestamptz |
question_reports
Жалобы пользователей на вопросы.
| Колонка | Тип | Описание |
|---|---|---|
| id | uuid, PK | |
| question_bank_id | uuid, FK → question_bank | |
| user_id | uuid, FK → users | |
| reason | text | |
| status | enum | open / resolved / dismissed |
| created_at | timestamptz |
user_stats
Агрегированная статистика по темам (обновляется после каждого теста).
| Колонка | Тип | Описание |
|---|---|---|
| id | uuid, PK | |
| user_id | uuid, FK → users | |
| stack | enum | |
| level | enum | |
| total_questions | integer | |
| correct_answers | integer | |
| tests_taken | integer | |
| last_test_at | timestamptz |
Unique constraint: (user_id, stack, level)
user_achievements
Бейджи и достижения (Phase 3).
| Колонка | Тип | Описание |
|---|---|---|
| id | uuid, PK | |
| user_id | uuid, FK → users | |
| achievement_code | varchar | Код достижения |
| earned_at | timestamptz |
user_question_log
Лог: какие вопросы пользователь видел (для дедупликации).
| Колонка | Тип | Описание |
|---|---|---|
| id | uuid, PK | |
| user_id | uuid, FK → users | |
| question_bank_id | uuid, FK → question_bank | |
| seen_at | timestamptz |
payments
Платежи.
| Колонка | Тип | Описание |
|---|---|---|
| id | uuid, PK | |
| user_id | uuid, FK → users | |
| subscription_id | uuid, FK → subscriptions | |
| amount | decimal | |
| currency | varchar, default 'RUB' | |
| status | enum | pending / succeeded / failed / refunded |
| provider | enum | yukassa / cloudpayments |
| external_id | varchar | ID у провайдера |
| created_at | timestamptz |
payment_events
Лог webhook-событий от платёжных систем.
| Колонка | Тип | Описание |
|---|---|---|
| id | uuid, PK | |
| payment_id | uuid, FK → payments, nullable | |
| provider | enum | |
| event_type | varchar | |
| payload | jsonb | Полный JSON от провайдера |
| processed | boolean, default false | |
| created_at | timestamptz |
audit_logs
Действия админов.
| Колонка | Тип | Описание |
|---|---|---|
| id | uuid, PK | |
| admin_id | uuid, FK → users | |
| action | varchar | ban_user / approve_question / ... |
| target_type | varchar | user / question / promo_code |
| target_id | uuid | |
| details | jsonb, nullable | |
| created_at | timestamptz |
notifications_log
История отправленных уведомлений.
| Колонка | Тип | Описание |
|---|---|---|
| id | uuid, PK | |
| user_id | uuid, FK → users | |
| channel | enum | email / in_app / telegram / push |
| template | varchar | verify_email / reset_password / trial_ending / ... |
| status | enum | sent / failed |
| created_at | timestamptz |
promo_codes
Промокоды.
| Колонка | Тип | Описание |
|---|---|---|
| id | uuid, PK | |
| code | varchar, unique | |
| discount_percent | integer | |
| max_uses | integer, nullable | |
| used_count | integer, default 0 | |
| valid_from | timestamptz | |
| valid_until | timestamptz | |
| created_at | timestamptz |
Индексы (ключевые)
users.email— uniquesessions.user_id— для списка устройствtests.user_id+tests.created_at— для историиquestion_bank.stack+question_bank.level+question_bank.status— для выборки вопросовuser_question_log.user_id+user_question_log.question_bank_id— для дедупликацииpayments.user_id— для истории платежей
Примечания
- Все PK — UUID v7 (сортируемые по времени)
- Все timestamps —
timestamptz(UTC) - JSONB используется для вариантов ответов и webhook payload
- Enum-значения для stack/level определяются один раз и используются во всех таблицах