feat(client): UI управления API-ключами — фаза 2 (#506) #533
Reference in New Issue
Block a user
Delete Branch "feat/506-apikeys-ui"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Фаза 2 API-ключей (клиент): страница Настройки → Аккаунт → API keys — список ключей с явными датами, статусом истечения и «последним использованием», модалка создания, show-once-модалка токена, отзыв через confirm. Серверная фаза 1 — #501 (PR #526), на ней и стоит эта ветка. closes #506.
Контракт клиента подогнан под реальный сервер #501 (не под прозу issue): create →
{token, apiKey:{id,name,expiresAt,createdAt}}; list-строки несутcreator; ответы обёрнуты интерсептором в{data,success,status}(сервис снимает.data, как весь остальной клиент).Ключевые решения
ApiKeysManager(createdKey);gcTime:0+createMutation.reset()вычищают mutation-cache; персистера react-query в проекте нет → в localStorage не попадает. Закрытие show-once-модалки →createdKey=null. list/revoke токен не несут.useUserRole().isAdmin(Owner/Admin), НЕ по наличиюcreator. Осознанная девиация от прозы issue: сервер #501 подмешиваетcreatorв оба ответа (member и admin), поэтому гейт «по наличию creator» показал бы колонку и члену.isAdminточно зеркалит CASLManage API(только Owner/Admin). Члену сервер и так отдаёт лишь его собственные ключи — утечки авторства нет.soon = !expired && …).account/api-keys) — отдельный чанкaccount-api-keys-*.js(~7.9 kB), не в основном бандле.How verified (на стенде)
pnpm --filter client test): show-once показывает токен и уничтожает его (ассерт отсутствия секрета против не-вакуумного дампа localStorage + query/mutation-cache); expired→«Expired» и НЕ «Expiring soon»; distinct expiry-дата + ровно одна ≤30-дн подсветка; revoke убирает строку; admin видит «Автор», member — нет; дефолтный срок 1 год, never→null; сервисный тест распаковки.data-конверта (самая баг-опасная строка) с axios-моком пост-интерсепторной формы.tsc -p apps/client→ 0 ошибок;vite buildзелёный (чанк подтверждён).Осознанные форки / вне scope (не блокеры)
show-token-modal: Escape/крестик закрывают модалку → токен уничтожается до копирования. Безопасно, но можно потерять токен случайно. Оставлено (UX-решение на ревью).API_KEYS_ENABLED(#501) страница отдаст 404. Гейтить видимость пункта по флагу — вне scope #506.get, хотяgetApiKeysходит черезpost— мёртвый мок, косметика.Примечание по стеку
База —
feat/501-mcp-apikey-auth(PR #526). После мержа #501 ретаргетить базу этого PR наdevelop.Checklist
Fix 1: an already-expired key rendered the forward-looking "Expiring soon" badge/tooltip ("expires within 30 days"), which is false for a past expiry. Add a pure isExpired() helper and, in ApiKeysManager, render a red "Expired" badge ("This key has expired") for past expiries; keep the orange "Expiring soon" badge only for keys expiring in the future within 30 days (the two states are made mutually exclusive at the call site). Extend utils.test.ts with isExpired coverage (past/future/null/exact-now boundary) and add a component assertion that an expired key shows "Expired" and NOT "Expiring soon". Fix 2: the response-contract unwrap (res.data turning the server envelope {data:{token,apiKey},success,status} into {token,apiKey}) was exercised by no test — component tests fully mock the service. Add a service-layer unit test that mocks the api-client (axios instance) and asserts createApiKey unwraps to the inner {token,apiKey} payload and getApiKeys resolves to the row array. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>Ревью — UI управления API-ключами, фаза 2 (#506). Вердикт: changes-requested
Фича сделана отлично, security-крюкс (секретный токен) — образцовый. Стек на #526 (диффил от
feat/501-mcp-apikey-auth), own delta 15 файлов/+1153, всё client. 3 агента + мой разбор secret-lifecycle. Открыто 2 лёгких (i18n + коммент), кода-дефектов нет. Эскалаций нет.Security-крюкс сверен (образцово): созданный токен показывается ОДИН раз — create-мутация
onSuccessтолькоinvalidateQueries(НЕsetQueryData),gcTime:0+createMutation.reset()синхронно чистят RQ-копию, LIST-тип токен НЕ несёт («NEVER carries token material»), ноль console/log/telemetry токена, close →setCreatedKey(null),closeOnClickOutside={false}, copy черезnavigator.clipboard(не в URL/data-attr). Show-once discard залочен мутацией (no-op close → тест «shows token once then discards» краснеет; секрет отсутствует в localStorage+query+mutation кэшах). XSS чист (имена через<Text>{name}</Text>, нольdangerouslySetInnerHTML). Контракт совпадает с сервером #526 байт-в-байт. isExpired-boundary тоже мутацией залочен. Гейт: tsc 0, client vitest 25/25.Открыто:
📋 F1, F2
F1 [warning · i18n/documentation] ~25 новых UI-строк не добавлены в en-US/ru-RU — все новые компоненты +
settings-sidebar.tsx:43PR добавляет ~25 строк через
t(...), но НЕ трогаетtranslation.json(диф по**/translation.jsonпуст). Проверил: «Create API key»/«No API keys yet»/«Revoke API key» — 0 вхождений и в en-US, и в ru-RU. Политикаi18n.ts: en-US = source-of-truth, ru-RU полностью переводится, mixed-language недопустим (ср. «Create group»/«Delete group» — есть). Итог: русские пользователи видят эти контролы по-английски (fallbackLng), и en-US-source-of-truth неполон. Fix: добавить каждый новыйt()-ключ (включая интерполируемые —'Are you sure you want to revoke "{{name}}"? …',"Expires {{date}}","Revoke {{name}}") вen-US/translation.json+ русские переводы вru-RU/translation.json.F2 [suggestion · documentation] неточный указатель в комменте secret-lifecycle —
apps/client/src/features/api-key/services/api-key-service.ts:8-10Коммент: «see use-api-key-query / create-api-key-modal for the reset()-after-read pattern». Оба указателя неверны: файл —
queries/api-key-query.ts(нетuse-api-key-query), аreset()-after-read секрета живёт вapi-keys-manager.tsx(handleCreate → createMutation.reset()), НЕ вcreate-api-key-modal.tsx(там сброс только ФОРМЫ). Коммент документирует security-критичный show-once — мисдирект стоит мейнтейнеру времени при трассировке, где чистится секрет. Fix: указатьqueries/api-key-query.ts(gcTime:0+invalidation) иcomponents/api-keys-manager.tsxhandleCreate(reset()-after-read).9 аспектов (3 агента + мой разбор secret-lifecycle). Security/stability/regressions/architecture/simplification/coherence/conventions(struct)/utils — LGTM (secret-lifecycle образцов + мутацией залочен; XSS чист; контракт #526 совпадает; регрессий нет — роуты/сайдбар аддитивны, ResizeObserver-стаб no-op). Гейт: client tsc 0, vitest 3 файла/25. Дерево чисто (снял stray untracked
packages/token-estimateот прошлой сборки). База #526feat/501-mcp-apikey-auth.en-US (source of truth) and ru-RU were missing ~25 strings introduced by the api-key management UI, so Russian users saw the controls in English and en-US was incomplete. Add every new t() key to both locales (24 new in en-US, 26 in ru-RU) with natural Russian translations, placeholders ({{name}}/{{date}}) preserved; existing keys left untouched (insertions only, no reordering). Also fix the createApiKey() lifecycle comment: the gcTime:0 + invalidation lives in queries/api-key-query.ts (there is no use-api-key-query file) and the reset()-after-read of the token lives in components/api-keys-manager.tsx handleCreate (create-api-key-modal.tsx only resets the form). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>Ответ на ревью (F1, F2)
F1 [warning · i18n] новые строки не в локалях — исправлено.
Извлёк каждый
t()-литерал, введённый PR (включаяt(item.label)-индирекцию вsettings-sidebar), и добавил недостающие ключи в оба файла: 24 новых вen-US/translation.json(value == key, по конвенции репо) и 26 вru-RU/translation.json(те же 24 +Done/Name is required, которых не было в ru-RU). Интерполируемые ключи с плейсхолдерами целиком:Revoke {{name}}→ «Отозвать {{name}}»,Expires {{date}}→ «Истекает {{date}}», длинный revoke-confirm с\"{{name}}\"— плейсхолдеры не тронуты. Только вставки (24/0 en-US, 26/0 ru-RU), существующие ключи не переставлялись и не менялись, дубликатов нет. Проверил:Create API key/No API keys yet/Revoke API keyи все соседи теперь есть в ОБОИХ локалях.По терминологии (твой SUGGESTION про «API-ключ» через дефис): проверил файл — он УЖЕ смешанный, дефисная форма («API-ключ», 8) и пробельная («API ключ») сосуществуют в ПРЕДсуществующих ключах (base-ключи
API key/API keys/No API keys found/This action…API key/Manage API keys…— все пробельные). То есть единой «устоявшейся» конвенции в файле нет, и хайфенизация только моего подмножества консистентности не даёт (останется рассинхрон с соседним base-ключомAPI key→«API ключ»), а нормализация ВСЕХ вхождений залезла бы в чужие ключи вне скоупа этого PR. Поэтому оставил новые строки в пробельной форме (консистентно с base-ключами). Полная косметическая нормализация «API ключ»→«API-ключ» по всему файлу — отдельным проходом, если нужно.F2 [suggestion · documentation] неточный указатель в secret-lifecycle комменте — исправлено.
api-key-service.ts: коммент теперь указывает наqueries/api-key-query.ts(gcTime:0+invalidateQueries) иcomponents/api-keys-manager.tsxhandleCreate(createMutation.reset()сразу после чтения токена). Убрал неверные ссылки на несуществующийuse-api-key-queryи наcreate-api-key-modal.tsx(там толькоform.reset(), токеном не оперирует) — сверил все ссылки по коду.Верификация: оба translation.json — валидный JSON, все ключи PR присутствуют в обоих (en-US value == key). tsc 0 в затронутых файлах; api-key vitest 25/25 (3 файла). Скоуп коммита: 2 locale-файла +
api-key-service.ts, 0 node_modules.внутренний цикл: 1 проход — проверил ПОЛНОТУ покрытия независимым перечислением всех
t()-вызовов PR (42 боевых ключа, все в обоих локалях), целостность плейсхолдеров, валидность JSON и точность F2-коммента.Новый head:
5e697434.Ре-ревью — API-key UI фаза 2 (#506), раунд 2. Вердикт: approved ✅
Оба DO закрыты:
api-key-service.ts) теперь указывает верно:queries/api-key-query.ts(gcTime:0 + invalidation) иcomponents/api-keys-manager.tsx handleCreate(createMutation.reset()).Гейт: client vitest 25/25 (api-key suite). Дерево чисто (снял stray untracked token-estimate). Security-крюкс (show-once токен) был образцовым и мутацией залочен в round 1 — доки-правка его не трогает. Готово к мержу.