feat(client): UI управления API-ключами — фаза 2 (#506) #533
Open
agent_coder
wants to merge 3 commits from
feat/506-apikeys-ui into feat/501-mcp-apikey-auth
pull from: feat/506-apikeys-ui
merge into: vvzvlad:feat/501-mcp-apikey-auth
vvzvlad:main
vvzvlad:feat/529-search-lexical
vvzvlad:feat/395-work-time
vvzvlad:feat/502-mcp-md-modes
vvzvlad:feat/492-incremental-render
vvzvlad:fix/549-hardbreak-md-canon
vvzvlad:feat/542-resolve-undo
vvzvlad:fix/505-drawio-strip-comments
vvzvlad:fix/503-hardbreak-canon
vvzvlad:docs/471-breadcrumb-permission
vvzvlad:fix/534-mcp-spaceid
vvzvlad:feat/481-version-coherence
vvzvlad:fix/535-list-seam-coalesce
vvzvlad:develop
vvzvlad:fix/520-recovery-escalation
vvzvlad:fix/525-insert-unloaded
vvzvlad:fix/522-internal-links
vvzvlad:fix/507-drawio-cyrillic
vvzvlad:fix/541-reconnect-timeout
vvzvlad:feat/501-mcp-apikey-auth
vvzvlad:feat/493-converter
vvzvlad:feat/370-page-versioning
vvzvlad:fix/515-code-emphasis
vvzvlad:fix/523-tree-dnd
vvzvlad:fix/517-toasts
vvzvlad:feat/495-tails
vvzvlad:feat/494-mcp-registry-guards
vvzvlad:feat/496-suggestions-audit
vvzvlad:integ/497-wave1
vvzvlad:feat/488-client-fsm
vvzvlad:ci/476-publish-gates
vvzvlad:feat/489-tool-resilience
vvzvlad:feat/487-terminal-lifecycle
vvzvlad:fix/486-iter1-prod-fires
vvzvlad:refactor/347-client-md-paste
vvzvlad:perf/479-getpage-cache
vvzvlad:refactor/449-write-invariants
vvzvlad:refactor/450-split-client
vvzvlad:feat/443-get-page-context
vvzvlad:feat/443-get-tree
vvzvlad:feat/443-search
vvzvlad:docs/415-lossy-descriptions
vvzvlad:feat/413-markdown-default
vvzvlad:refactor/412-camelcase-tool-names
vvzvlad:feat/425-drawio-graph
vvzvlad:fix/464-diffdocs-cpu-guard
vvzvlad:refactor/411-update-page-markdown
vvzvlad:feat/424-drawio-rules
vvzvlad:fix/409-invalid-node-validation
vvzvlad:refactor/448-generate-inventory
vvzvlad:refactor/445-execute-mapping
vvzvlad:refactor/446-derive-client-types
vvzvlad:fix/447-registry-stamp-ci
vvzvlad:fix/444-agent-loop-guards
vvzvlad:fix/396-sendnow-detached-run
vvzvlad:fix/452-immutable-cache-control
vvzvlad:docs/footnote-authoring-comment-cleanup
vvzvlad:feat/437-error-diagnostics
vvzvlad:fix/442-cursor-pagination
vvzvlad:feat/419-footnote-normalize
vvzvlad:perf/399-comment-resolve-async
vvzvlad:perf/435-collab-token-cache
vvzvlad:perf/344-background-rerenders
vvzvlad:perf/348-backend-lowhanging
vvzvlad:feat/423-drawio-crud
vvzvlad:perf/401-collab-hotpath
vvzvlad:feat/430-live-reconnect
vvzvlad:perf/400-collab-session
vvzvlad:refactor/414-dedup-node-ops
vvzvlad:feat/417-new-comments-signal
vvzvlad:feat/408-createcomment-hints
vvzvlad:feat/407-persist-tool-errors
vvzvlad:feat/420-footnote-render
vvzvlad:dummy-review/mcp-hang-fix
vvzvlad:dummy-review/mcp-hang-base
vvzvlad:feat/381-resumable-sse-pr1
vvzvlad:feat/git-sync-2
vvzvlad:docs/agents-workspace-build-order
vvzvlad:feature/offline-sync
vvzvlad:perf/342-code-splitting
vvzvlad:perf/346-compression-cache
vvzvlad:feat/196-multi-cursor
vvzvlad:perf/343-typing-latency
vvzvlad:docs/how-to-test
vvzvlad:fix/ai-sdk-partial-output-oom
vvzvlad:test/351-generative-converter
vvzvlad:feat/371-roles-catalog
vvzvlad:refactor/345-server-converter
vvzvlad:refactor/294-spec-registry-cont
vvzvlad:fix/363-migration-order
vvzvlad:fix/e2e-callout-and-gate-build
vvzvlad:fix/docker-re2-toolchain
vvzvlad:feat/git-sync
vvzvlad:fix/media-roundtrip-stability
vvzvlad:fix/340-comment-panel-perf
vvzvlad:fix/332-deferred-tools
vvzvlad:fix/329-ephemeral-suggestions
vvzvlad:fix/330-search-in-page
vvzvlad:fix/328-resolved-anchor-spam
vvzvlad:fix/331-intraline-diff
vvzvlad:fix/324-coverage-gate
vvzvlad:fix/325-mobile-390
vvzvlad:feat/293-A-git-sync-package
vvzvlad:feat/300-avatar-oklch
vvzvlad:fix/321-banner-mobile
vvzvlad:feat/300-avatar-colors
vvzvlad:feat/315-comment-suggestions
vvzvlad:feat/scroll-restore-stable-wait
vvzvlad:feat/300-agent-avatar-stack
vvzvlad:feat/300-avatar-polish
vvzvlad:refactor/294-tool-spec-registry
vvzvlad:feat/scroll-restore-ux
vvzvlad:fix/responsive-tablet-sidebar
vvzvlad:feature/ai-chat-page-change-observability
vvzvlad:image-inline-center
vvzvlad:fix/283-short-remap-title
vvzvlad:fix/283-slash-layout
vvzvlad:image-inline-row
vvzvlad:feat/276-ai-chat-dock
vvzvlad:fix/269-table-menu-refocus
vvzvlad:docs/dev-stand-guide
vvzvlad:feat/266-scroll-position
vvzvlad:fix/260-collab-docname-slugid
vvzvlad:test/244-phase2-tail
vvzvlad:fix/262-reindex-progress-realtime
vvzvlad:fix/258-changelog-compare-links
vvzvlad:fix/244-dataloss-bugs
vvzvlad:feat/246-spoiler
vvzvlad:feat/221-image-captions
vvzvlad:test/244-part-b
vvzvlad:feat/251-intentional-clear
vvzvlad:fix/embeddings-reindex-progress
vvzvlad:refactor/193-tool-spec-registry
vvzvlad:fix/255-ws-redis-adapter-leak
vvzvlad:fix/252-e2e-open-handles
vvzvlad:feat/229-catalog-yaml
vvzvlad:feat/243-blob-sandbox
vvzvlad:feat/228-inline-footnotes
vvzvlad:fix/qa-ui-bugs-216-218
vvzvlad:feature/agent-roles-catalog
vvzvlad:fix/share-alias-rename
vvzvlad:fix/ai-chat-empty-render
vvzvlad:feat/191-chat-doc-binding
vvzvlad:feat/201-temporary-notes
vvzvlad:feat/198-interrupt-agent
vvzvlad:feat/ai-chat-full-history
vvzvlad:feat/199-ai-generate-title
vvzvlad:feat/205-share-aliases
vvzvlad:batch/issues-189-187-170
vvzvlad:feat/170-mcp-test-button
vvzvlad:feat/189-context-badge
vvzvlad:feat/198-interrupt-agent-send-now
vvzvlad:fix/issues-190-159
vvzvlad:fix/ai-chat-new-chat-during-stream
vvzvlad:fix/ai-chat-stream-perf
vvzvlad:batch/issues-2026-06-25
vvzvlad:feat/ai-chat-persistent-history
vvzvlad:fix/ai-chat-copy-chat-wysiwyg
vvzvlad:fix/ai-stream-reset-resilience
vvzvlad:fix/ai-stream-undici-timeout
vvzvlad:fix/footnote-review-1227-followup
vvzvlad:fix/ai-chat-token-counter-realtime
vvzvlad:docs/manual-qa-test-plan
No Reviewers
Labels
Clear labels
epic
needs-human
review/approved
review/changes-requested
review/needs
возможно, баг
Large multi-phase effort spanning many changes
эскалация: нужно решение человека
в последнем ревью нет открытых blocking-находок
последнее ревью оставило открытые blocking-находки
head не ревьюился (head != reviewed_head)
Похоже на баг, но требует подтверждения/воспроизведения; чиним, если повторится
No Label
review/approved
Milestone
No items
No Milestone
Projects
Clear projects
No project
No Assignees
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: vvzvlad/gitmost#533
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking 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 — доки-правка его не трогает. Готово к мержу.
View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.