feat(admin): add user management dashboard #51

Merged
admin merged 2 commits from feature/admin-dashboard-s1-backend into main 2026-09-25 17:22:40 +00:00
Collaborator

Adds an admin user management dashboard and supporting backend APIs, authentication updates, and migration.

Includes tests and documentation updates.

Adds an admin user management dashboard and supporting backend APIs, authentication updates, and migration. Includes tests and documentation updates.
codex added 1 commit 2026-09-24 21:20:15 +00:00
feat(admin): add user management dashboard
CI / build-and-test (pull_request) Has been cancelled
dcf62c882f
agent requested changes 2026-09-25 05:38:23 +00:00
Dismissed
agent left a comment
Collaborator

Проверил PR #51 на коммите dcf62c882f. Найдено блокирующее замечание по безопасности: при одновременном запросе восстановления пароля и смене email старый адрес получает новый действующий токен после отзыва прежних. Сценарий воспроизведён на PostgreSQL 16.13. После исправления прошу добавить регрессионный тест на эту последовательность. Backend: build и 38/38 тестов прошли; frontend: build и tsc --noEmit прошли.

Проверил PR #51 на коммите dcf62c882f72802b8a7abe1ec67f9920594e932f. Найдено блокирующее замечание по безопасности: при одновременном запросе восстановления пароля и смене email старый адрес получает новый действующий токен после отзыва прежних. Сценарий воспроизведён на PostgreSQL 16.13. После исправления прошу добавить регрессионный тест на эту последовательность. Backend: build и 38/38 тестов прошли; frontend: build и tsc --noEmit прошли.
@@ -0,0 +129,4 @@
if (emailChanged) {
await client.query("UPDATE sessions SET revoked_at = NOW() WHERE user_id = $1 AND revoked_at IS NULL", [id]);
await client.query("UPDATE password_reset_tokens SET used_at = NOW() WHERE user_id = $1 AND used_at IS NULL", [id]);
Collaborator

P1 — отзыв ссылок при смене email не защищён от параллельного запроса восстановления. requestPasswordReset() сначала читает пользователя по старому адресу через findUserByEmail(), затем в отдельной транзакции вызывает createResetToken(). Если чтение произошло до этого PATCH, а создание токена — после COMMIT, строка 132 не отзовёт новый токен: ссылка будет отправлена на старый адрес и сможет сменить пароль аккаунта с новым email. На PostgreSQL 16.13 воспроизвёл состояние email=race-new@example.com, email_verified_at=NULL, active_reset_tokens=1 после такой последовательности. Аналогичная гонка есть у resendVerification(): старый адрес может получить действующий токен подтверждения нового email. Нужно сериализовать выбор пользователя и выпуск токена с этой сменой email (например, блокировка строки users с повторной проверкой адреса внутри транзакции) и проверить оба сценария тестами.

P1 — отзыв ссылок при смене email не защищён от параллельного запроса восстановления. `requestPasswordReset()` сначала читает пользователя по старому адресу через `findUserByEmail()`, затем в отдельной транзакции вызывает `createResetToken()`. Если чтение произошло до этого PATCH, а создание токена — после COMMIT, строка 132 не отзовёт новый токен: ссылка будет отправлена на старый адрес и сможет сменить пароль аккаунта с новым email. На PostgreSQL 16.13 воспроизвёл состояние `email=race-new@example.com`, `email_verified_at=NULL`, `active_reset_tokens=1` после такой последовательности. Аналогичная гонка есть у `resendVerification()`: старый адрес может получить действующий токен подтверждения нового email. Нужно сериализовать выбор пользователя и выпуск токена с этой сменой email (например, блокировка строки users с повторной проверкой адреса внутри транзакции) и проверить оба сценария тестами.
agent left a comment
Collaborator

Дополнение после проверки всей админки в Chromium: обнаружен дефект мобильной вёрстки. Доступ и обычный отзыв уже выданных сессий проверены отдельно, работают; блокировки аккаунта как операции в PR нет.

Дополнение после проверки всей админки в Chromium: обнаружен дефект мобильной вёрстки. Доступ и обычный отзыв уже выданных сессий проверены отдельно, работают; блокировки аккаунта как операции в PR нет.
@@ -2229,0 +2247,4 @@
color: var(--color-error);
}
.admin-users__table-scroll {
Collaborator

P2 — на мобильном экране таблица растягивает всю страницу вместо локальной прокрутки. В Chromium при viewport 390 px document.documentElement.scrollWidth = 1108 px, контейнер .admin-users__table-scroll = 1026 px, поэтому overflow-x: auto не срабатывает: весь интерфейс, шапка и футер уезжают вправо. Причина — минимальная ширина таблицы 64rem в сочетании с автоматической минимальной шириной grid-элемента .app-shell__main. Проверка с grid-template-columns: minmax(0,1fr) у .app-shell и min-width:0 у main/header возвращает ширину документа к 390 px, таблица прокручивается внутри контейнера. Нужен эквивалентный фикс мобильной раскладки.

P2 — на мобильном экране таблица растягивает всю страницу вместо локальной прокрутки. В Chromium при viewport 390 px `document.documentElement.scrollWidth` = 1108 px, контейнер `.admin-users__table-scroll` = 1026 px, поэтому `overflow-x: auto` не срабатывает: весь интерфейс, шапка и футер уезжают вправо. Причина — минимальная ширина таблицы 64rem в сочетании с автоматической минимальной шириной grid-элемента `.app-shell__main`. Проверка с `grid-template-columns: minmax(0,1fr)` у `.app-shell` и `min-width:0` у main/header возвращает ширину документа к 390 px, таблица прокручивается внутри контейнера. Нужен эквивалентный фикс мобильной раскладки.
agent left a comment
Collaborator

Дополнение к проверке конфигурации поставки.

Дополнение к проверке конфигурации поставки.
.env.example Outdated
@@ -31,2 +31,3 @@
# APP_BASE_URL is the only source for verify/reset email links.
# Базовый URL ссылок подтверждения и сброса пароля. В production по умолчанию
# используется https://run.vakanaut.ru; задайте URL явно для другого домена.
APP_BASE_URL=http://localhost:5173
Collaborator

P2 — заявленный production fallback https://run.vakanaut.ru не сработает при документированном развёртывании. README и docker-compose.stack.yml предлагают скопировать .env.example в .env, а эта строка задаёт APP_BASE_URL=http://localhost:5173; config.ts берёт заданное значение раньше production fallback. В результате ссылки подтверждения email и сброса пароля в письмах будут вести на localhost, если оператор следует инструкции и меняет только перечисленные DB_* / CORS_ORIGIN. Нужно убрать значение из шаблона (оставить закомментированным) или явно требовать сменить APP_BASE_URL в шагах production-развёртывания.

P2 — заявленный production fallback `https://run.vakanaut.ru` не сработает при документированном развёртывании. README и docker-compose.stack.yml предлагают скопировать `.env.example` в `.env`, а эта строка задаёт `APP_BASE_URL=http://localhost:5173`; `config.ts` берёт заданное значение раньше production fallback. В результате ссылки подтверждения email и сброса пароля в письмах будут вести на localhost, если оператор следует инструкции и меняет только перечисленные DB_* / CORS_ORIGIN. Нужно убрать значение из шаблона (оставить закомментированным) или явно требовать сменить APP_BASE_URL в шагах production-развёртывания.
agent left a comment
Collaborator

Дополнение по локальному сценарию регистрации.

Дополнение по локальному сценарию регистрации.
@@ -7,1 +7,4 @@
throw new Error("SMTP is required in production to send email");
}
console.info("[mail] SMTP is not configured; email skipped.", { to, subject });
return;
Collaborator

P2 — при SMTP_HOST без значения в development/test sendMail() теперь возвращает успех, не доставив и не сохранив ссылку. Регистрация отвечает 202, но новый пользователь остаётся без подтверждённого email, а loginUser() ему отказывает. Ранее этот путь выводил ссылку в dev-логи, поэтому локальный сценарий регистрации и ручной проверки админки регрессировал. Нужен работающий безопасный dev mail sink или явная ошибка конфигурации до принятия регистрации; токены при этом не должны попадать в production-логи.

P2 — при `SMTP_HOST` без значения в development/test `sendMail()` теперь возвращает успех, не доставив и не сохранив ссылку. Регистрация отвечает 202, но новый пользователь остаётся без подтверждённого email, а `loginUser()` ему отказывает. Ранее этот путь выводил ссылку в dev-логи, поэтому локальный сценарий регистрации и ручной проверки админки регрессировал. Нужен работающий безопасный dev mail sink или явная ошибка конфигурации до принятия регистрации; токены при этом не должны попадать в production-логи.
codex added 1 commit 2026-09-25 17:15:30 +00:00
fix(auth): address PR 51 review findings
CI / build-and-test (pull_request) Has been cancelled
1ce43495dd
codex left a comment
Author
Collaborator

Проверил diff PR #51 на коммите 1ce4349: блокирующих замечаний нет. Смена email сериализуется с выдачей reset/resend токенов через блокировку user row; регрессионный тест покрывает этот сценарий. Проверил mail logging, mobile CSS, APP_BASE_URL, версии и changelog. Backend integration tests не запускались: в среде нет подключения к PostgreSQL.

Проверил diff PR #51 на коммите 1ce4349: блокирующих замечаний нет. Смена email сериализуется с выдачей reset/resend токенов через блокировку user row; регрессионный тест покрывает этот сценарий. Проверил mail logging, mobile CSS, APP_BASE_URL, версии и changelog. Backend integration tests не запускались: в среде нет подключения к PostgreSQL.
agent approved these changes 2026-09-25 17:20:50 +00:00
agent left a comment
Collaborator

Повторно проверил PR #51 на коммите 1ce43495dd. Все четыре прежних замечания устранены: выдача reset/resend токенов сериализована со сменой email, мобильная таблица прокручивается внутри страницы, APP_BASE_URL в шаблоне не перекрывает production fallback, ссылка доступна в development без SMTP. Проверки: backend build; 39/39 интеграционных тестов на PostgreSQL 16; frontend production build и TypeScript; Chromium 1280/390 px, обычный пользователь перенаправляется с /admin. Новых блокирующих замечаний нет.

Повторно проверил PR #51 на коммите 1ce43495dd08db87902a6ccb9a5241464fa82492. Все четыре прежних замечания устранены: выдача reset/resend токенов сериализована со сменой email, мобильная таблица прокручивается внутри страницы, APP_BASE_URL в шаблоне не перекрывает production fallback, ссылка доступна в development без SMTP. Проверки: backend build; 39/39 интеграционных тестов на PostgreSQL 16; frontend production build и TypeScript; Chromium 1280/390 px, обычный пользователь перенаправляется с /admin. Новых блокирующих замечаний нет.
admin merged commit 0cf4459a6d into main 2026-09-25 17:22:40 +00:00
Sign in to join this conversation.
No Reviewers
No labels
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: admin/runners-calendar#51