feat(dashboard): add managed API keys page#195
Conversation
…tikey # Conflicts: # internal/auditlog/reader_postgresql.go # internal/auditlog/reader_sqlite.go # internal/auditlog/store_postgresql.go # internal/auditlog/store_postgresql_test.go # internal/auditlog/store_sqlite.go
📝 WalkthroughWalkthroughThis PR adds an API Keys admin UI and client module, persists and surfaces auth-method metadata through audit-log schema/reads/writes and middleware enrichment, and shows authentication status in execution-plan charts (templates, JS model, CSS). Tests for client behavior, timestamp formatting, chart outputs, and DB store adjustments were added/updated. Changes
Sequence DiagramssequenceDiagram
actor Admin
participant Dashboard as Admin Dashboard
participant Server as API Server
participant DB as Database
rect rgba(100, 150, 255, 0.5)
Note over Admin,DB: Create API Key Flow
Admin->>Dashboard: Open "Create API Key" and submit form
Dashboard->>Dashboard: Normalize expires_at to UTC end-of-day
Dashboard->>Server: POST /admin/api/v1/auth-keys
Server->>DB: Insert new auth key row
Server->>Server: Enrich audit log with auth_method
Server->>DB: Insert audit entry
Server-->>Dashboard: Return issued key value
Dashboard->>Admin: Show issued key + copy button
end
rect rgba(100, 200, 100, 0.5)
Note over Admin,DB: Deactivate API Key Flow
Admin->>Dashboard: Click "Deactivate"
Dashboard->>Admin: Confirm
Admin->>Dashboard: Confirm
Dashboard->>Server: POST /admin/api/v1/auth-keys/{id}/deactivate
Server->>DB: Mark key inactive
Server->>Server: Enrich audit log with auth_method
Server->>DB: Insert audit entry
Server-->>Dashboard: Success
Dashboard->>Dashboard: Refresh list
end
rect rgba(200, 150, 100, 0.5)
Note over Admin,DB: Fetch & Display Keys
Admin->>Dashboard: Navigate to API Keys page
Dashboard->>Server: GET /admin/api/v1/auth-keys
Server->>DB: Query auth keys
Server-->>Dashboard: Return key list
Dashboard->>Admin: Render table with statuses and expirations
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
internal/server/auth.go (1)
74-85:⚠️ Potential issue | 🟠 MajorRecord
AuthMethodAPIKeybefore authentication so failed attempts are attributed.Right now
auth_methodis only set after successfulAuthenticate, so rejected managed-key attempts lose auth-method attribution in audit traces.🔧 Proposed fix
if authenticator != nil && authenticator.Enabled() { + auditlog.EnrichEntryWithAuthMethod(c, auditlog.AuthMethodAPIKey) authKeyID, err := authenticator.Authenticate(c.Request().Context(), token) if err == nil { ctx := core.WithAuthKeyID(c.Request().Context(), authKeyID) c.SetRequest(c.Request().WithContext(ctx)) auditlog.EnrichEntryWithAuthKeyID(c, authKeyID) - auditlog.EnrichEntryWithAuthMethod(c, auditlog.AuthMethodAPIKey) return next(c) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/server/auth.go` around lines 74 - 85, Before calling authenticator.Authenticate, set the audit method to AuthMethodAPIKey so failed managed-key attempts are attributed; specifically, move or add the call to auditlog.EnrichEntryWithAuthMethod(c, auditlog.AuthMethodAPIKey) to occur before invoking authenticator.Authenticate (in the block guarded by authenticator != nil && authenticator.Enabled()), so that authenticationErrorWithAudit and authFailureMessage will include the method even on failure while leaving existing ctx/authKeyID handling unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@internal/admin/dashboard/static/js/modules/auth-keys.js`:
- Around line 66-71: The copyAuthKeyValue method currently calls
navigator.clipboard.writeText(this.authKeyIssuedValue) without handling
rejection or absence of the Clipboard API; update copyAuthKeyValue to first
feature-detect navigator.clipboard and, if available, call
writeText(...).then(...) and add a .catch(...) to handle failures (set a new
error flag like this.authKeyCopyError = true, reset authKeyCopied appropriately,
and optionally console.error). If navigator.clipboard is not available,
implement a synchronous fallback that creates a temporary textarea, selects its
content and uses document.execCommand('copy') with try/catch to set
authKeyCopied or authKeyCopyError accordingly; ensure you clear the temporary
element and reset flags after 2s as existing logic does.
In `@internal/auditlog/middleware.go`:
- Around line 391-402: EnrichEntryWithAuthMethod currently assigns any string to
LogEntry.AuthMethod; update the EnrichEntryWithAuthMethod function to first trim
whitespace from the incoming method string, return early if the trimmed value is
empty, and validate it against a small allowlist of acceptable auth method
identifiers (e.g., "password", "oauth", "sso", "api_key", "magic_link",
"unknown"); only set entry.AuthMethod when the trimmed value is one of those
allowed values and keep existing nil-checks for c.Get(string(LogEntryKey)) and
type-assertion to *LogEntry as currently done.
In `@internal/auditlog/store_postgresql.go`:
- Around line 88-91: The migration slice in
internal/auditlog/store_postgresql.go contains a duplicated SQL entry "ALTER
TABLE audit_logs ADD COLUMN IF NOT EXISTS auth_method TEXT"; locate the
migrations array (the slice of ALTER TABLE statements) and remove the duplicate
entry so each column migration appears only once (ensure "user_path" and a
single "auth_method" remain), updating the migrations slice where those ALTER
TABLE strings are defined.
In `@internal/auditlog/store_sqlite.go`:
- Around line 73-76: Remove the duplicate "ALTER TABLE audit_logs ADD COLUMN
auth_method TEXT" entry from the migrations list so the migrations slice only
contains one addition of auth_method; edit the migrations variable (the
slice/array that includes "ALTER TABLE audit_logs ADD COLUMN auth_method TEXT",
"ALTER TABLE audit_logs ADD COLUMN user_path TEXT", etc.) and delete the
repeated auth_method string to keep the migration entries unique.
---
Outside diff comments:
In `@internal/server/auth.go`:
- Around line 74-85: Before calling authenticator.Authenticate, set the audit
method to AuthMethodAPIKey so failed managed-key attempts are attributed;
specifically, move or add the call to auditlog.EnrichEntryWithAuthMethod(c,
auditlog.AuthMethodAPIKey) to occur before invoking authenticator.Authenticate
(in the block guarded by authenticator != nil && authenticator.Enabled()), so
that authenticationErrorWithAudit and authFailureMessage will include the method
even on failure while leaving existing ctx/authKeyID handling unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 9e8fd29a-f5f0-4186-8414-46370ffb2f35
📒 Files selected for processing (21)
internal/admin/dashboard/static/css/dashboard.cssinternal/admin/dashboard/static/js/dashboard.jsinternal/admin/dashboard/static/js/modules/auth-keys.jsinternal/admin/dashboard/static/js/modules/auth-keys.test.jsinternal/admin/dashboard/static/js/modules/dashboard-layout.test.jsinternal/admin/dashboard/static/js/modules/execution-plans-layout.test.jsinternal/admin/dashboard/static/js/modules/execution-plans.jsinternal/admin/dashboard/static/js/modules/execution-plans.test.jsinternal/admin/dashboard/static/js/modules/timestamp-format.test.jsinternal/admin/dashboard/templates/execution-plan-chart.htmlinternal/admin/dashboard/templates/index.htmlinternal/admin/dashboard/templates/layout.htmlinternal/auditlog/auditlog.gointernal/auditlog/middleware.gointernal/auditlog/reader_mongodb.gointernal/auditlog/reader_postgresql.gointernal/auditlog/reader_sqlite.gointernal/auditlog/store_postgresql.gointernal/auditlog/store_postgresql_test.gointernal/auditlog/store_sqlite.gointernal/server/auth.go
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
internal/server/auth.go (1)
43-52: 🧹 Nitpick | 🔵 TrivialSkipped paths leave
AuthMethodempty.When a request path matches
skipPaths, the middleware returns without settingAuthMethod. This means audit entries for skipped paths will have an empty auth method even when auth is configured for other routes. If this is intentional (skipped paths don't undergo auth evaluation), no change needed. Otherwise, consider settingAuthMethodNoKeyor a dedicated"skipped"value.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/server/auth.go` around lines 43 - 52, When a request matches the skipPaths branch the middleware returns early without setting AuthMethod, leaving audit records with an empty auth method; update the skipPaths handling (the loop that checks skipPaths and requestPath and currently does "return next(c)") to set the audit/auth indicator (e.g. set AuthMethod = AuthMethodNoKey or the string "skipped" in the same context/storage you use elsewhere) immediately before returning, so skipped routes produce a consistent auth method value.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@internal/admin/dashboard/static/js/modules/auth-keys.js`:
- Around line 37-40: When handleFetchResponse(res, 'auth keys') returns false,
we should surface the HTTP failure instead of silently clearing the list: in the
failure branch in auth-keys.js (the block that currently sets this.authKeys = []
and returns) set the component/state variable authKeyError (or assign an error
message from the response) to indicate a load failure and do not clobber
authKeys; use handleFetchResponse and the response object to distinguish a 401
vs other errors so only 401 handling remains unchanged and all other non-2xx
responses set authKeyError (and early-return) rather than clearing the table.
In `@internal/admin/dashboard/templates/index.html`:
- Around line 238-240: The template buttons currently call openAuthKeyForm() and
can trigger clearing of the one-time authKeyIssuedValue while the key is
visible; update the logic in
internal/admin/dashboard/static/js/modules/auth-keys.js so openAuthKeyForm() and
closeAuthKeyForm() do not unconditionally clear authKeyIssuedValue — add checks
for a submission-in-flight flag (e.g., isIssuing) and whether the editor is
already open (e.g., isAuthEditorOpen) before clearing, set/clear that isIssuing
flag around submitAuthKeyForm() so submitAuthKeyForm() will set
authKeyIssuedValue and ensure it reopens the editor (set isAuthEditorOpen) if
the request completes after a manual close; also guard the template buttons (the
Create API Key and the other control mentioned) to not call openAuthKeyForm()
when isIssuing is true to avoid losing the only visible key.
- Around line 362-364: The empty-state paragraph currently shows "No API keys
yet" even when an authKeyError exists; update its x-show condition to also
require that authKeyError is falsy so it only renders when there is no load
error (i.e., change the condition on the <p class="empty-state"> that references
authKeys, authKeysLoading, authError, and authKeysAvailable to also include
!authKeyError) to prevent the empty message appearing alongside real error
messages.
In `@internal/auditlog/middleware_auth_method_test.go`:
- Around line 11-47: Add assertions for the remaining allowed auth-method values
in the existing tests: call EnrichEntryWithAuthMethod(c, "no_key") and assert
entry.AuthMethod == AuthMethodNoKey, and call EnrichEntryWithAuthMethod(c,
"unknown") and assert entry.AuthMethod == "unknown" (or the corresponding
constant if defined), keeping the same context setup (echo.New,
httptest.NewRequest, c.Set(string(LogEntryKey), entry)) and trimming/validation
expectations used with EnrichEntryWithAuthMethod so the test suite covers all
permitted values.
---
Outside diff comments:
In `@internal/server/auth.go`:
- Around line 43-52: When a request matches the skipPaths branch the middleware
returns early without setting AuthMethod, leaving audit records with an empty
auth method; update the skipPaths handling (the loop that checks skipPaths and
requestPath and currently does "return next(c)") to set the audit/auth indicator
(e.g. set AuthMethod = AuthMethodNoKey or the string "skipped" in the same
context/storage you use elsewhere) immediately before returning, so skipped
routes produce a consistent auth method value.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 99468d40-a6e1-426b-b7d0-e0e818a36f0a
📒 Files selected for processing (9)
internal/admin/dashboard/static/js/modules/auth-keys.jsinternal/admin/dashboard/static/js/modules/auth-keys.test.jsinternal/admin/dashboard/templates/index.htmlinternal/auditlog/middleware.gointernal/auditlog/middleware_auth_method_test.gointernal/auditlog/store_postgresql.gointernal/auditlog/store_sqlite.gointernal/server/auth.gointernal/server/auth_test.go
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@internal/admin/dashboard/static/js/modules/auth-keys.js`:
- Around line 233-273: The deactivateAuthKey method directly calls
window.confirm which breaks the module's testability; change that call to use
the injected global.confirm (or the module's injected confirm reference) instead
of window.confirm so tests can mock it; update the conditional at the start of
deactivateAuthKey to call global.confirm('Deactivate key "' + key.name + '"?
This cannot be undone.') and leave all other logic (authKeyDeactivatingID,
authKeyError, fetchAuthKeys, etc.) unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 47d20864-44f8-4c29-89af-e71137068ec6
📒 Files selected for processing (7)
internal/admin/dashboard/static/js/modules/auth-keys.jsinternal/admin/dashboard/static/js/modules/auth-keys.test.jsinternal/admin/dashboard/static/js/modules/dashboard-layout.test.jsinternal/admin/dashboard/templates/index.htmlinternal/auditlog/middleware_auth_method_test.gointernal/server/auth.gointernal/server/auth_test.go
| async deactivateAuthKey(key) { | ||
| if (!key || !key.active) { | ||
| return; | ||
| } | ||
| if (!window.confirm('Deactivate key "' + key.name + '"? This cannot be undone.')) { | ||
| return; | ||
| } | ||
|
|
||
| this.authKeyDeactivatingID = key.id; | ||
| this.authKeyError = ''; | ||
| this.authKeyNotice = ''; | ||
|
|
||
| try { | ||
| const res = await fetch('/admin/api/v1/auth-keys/' + encodeURIComponent(key.id) + '/deactivate', { | ||
| method: 'POST', | ||
| headers: this.headers() | ||
| }); | ||
| if (res.status === 503) { | ||
| this.authKeysAvailable = false; | ||
| this.authKeyError = 'Auth keys feature is unavailable.'; | ||
| return; | ||
| } | ||
| if (res.status === 401) { | ||
| this.authError = true; | ||
| this.needsAuth = true; | ||
| this.authKeyError = 'Authentication required.'; | ||
| return; | ||
| } | ||
| if (res.status !== 204) { | ||
| this.authKeyError = await this._authKeyResponseMessage(res, 'Failed to deactivate key.'); | ||
| return; | ||
| } | ||
| await this.fetchAuthKeys(); | ||
| this.authKeyNotice = 'Key "' + key.name + '" deactivated.'; | ||
| } catch (e) { | ||
| console.error('Failed to deactivate auth key:', e); | ||
| this.authKeyError = 'Failed to deactivate key.'; | ||
| } finally { | ||
| this.authKeyDeactivatingID = ''; | ||
| } | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Use the injected global reference for confirm to maintain testability.
The module uses global.navigator, global.document etc. for testability, but line 237 directly references window.confirm. While window equals global at runtime, using global.confirm (or a parameter) would be more consistent and allow mocking in tests if needed.
♻️ Suggested fix
- if (!window.confirm('Deactivate key "' + key.name + '"? This cannot be undone.')) {
+ if (!global.confirm('Deactivate key "' + key.name + '"? This cannot be undone.')) {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| async deactivateAuthKey(key) { | |
| if (!key || !key.active) { | |
| return; | |
| } | |
| if (!window.confirm('Deactivate key "' + key.name + '"? This cannot be undone.')) { | |
| return; | |
| } | |
| this.authKeyDeactivatingID = key.id; | |
| this.authKeyError = ''; | |
| this.authKeyNotice = ''; | |
| try { | |
| const res = await fetch('/admin/api/v1/auth-keys/' + encodeURIComponent(key.id) + '/deactivate', { | |
| method: 'POST', | |
| headers: this.headers() | |
| }); | |
| if (res.status === 503) { | |
| this.authKeysAvailable = false; | |
| this.authKeyError = 'Auth keys feature is unavailable.'; | |
| return; | |
| } | |
| if (res.status === 401) { | |
| this.authError = true; | |
| this.needsAuth = true; | |
| this.authKeyError = 'Authentication required.'; | |
| return; | |
| } | |
| if (res.status !== 204) { | |
| this.authKeyError = await this._authKeyResponseMessage(res, 'Failed to deactivate key.'); | |
| return; | |
| } | |
| await this.fetchAuthKeys(); | |
| this.authKeyNotice = 'Key "' + key.name + '" deactivated.'; | |
| } catch (e) { | |
| console.error('Failed to deactivate auth key:', e); | |
| this.authKeyError = 'Failed to deactivate key.'; | |
| } finally { | |
| this.authKeyDeactivatingID = ''; | |
| } | |
| } | |
| async deactivateAuthKey(key) { | |
| if (!key || !key.active) { | |
| return; | |
| } | |
| if (!global.confirm('Deactivate key "' + key.name + '"? This cannot be undone.')) { | |
| return; | |
| } | |
| this.authKeyDeactivatingID = key.id; | |
| this.authKeyError = ''; | |
| this.authKeyNotice = ''; | |
| try { | |
| const res = await fetch('/admin/api/v1/auth-keys/' + encodeURIComponent(key.id) + '/deactivate', { | |
| method: 'POST', | |
| headers: this.headers() | |
| }); | |
| if (res.status === 503) { | |
| this.authKeysAvailable = false; | |
| this.authKeyError = 'Auth keys feature is unavailable.'; | |
| return; | |
| } | |
| if (res.status === 401) { | |
| this.authError = true; | |
| this.needsAuth = true; | |
| this.authKeyError = 'Authentication required.'; | |
| return; | |
| } | |
| if (res.status !== 204) { | |
| this.authKeyError = await this._authKeyResponseMessage(res, 'Failed to deactivate key.'); | |
| return; | |
| } | |
| await this.fetchAuthKeys(); | |
| this.authKeyNotice = 'Key "' + key.name + '" deactivated.'; | |
| } catch (e) { | |
| console.error('Failed to deactivate auth key:', e); | |
| this.authKeyError = 'Failed to deactivate key.'; | |
| } finally { | |
| this.authKeyDeactivatingID = ''; | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/admin/dashboard/static/js/modules/auth-keys.js` around lines 233 -
273, The deactivateAuthKey method directly calls window.confirm which breaks the
module's testability; change that call to use the injected global.confirm (or
the module's injected confirm reference) instead of window.confirm so tests can
mock it; update the conditional at the start of deactivateAuthKey to call
global.confirm('Deactivate key "' + key.name + '"? This cannot be undone.') and
leave all other logic (authKeyDeactivatingID, authKeyError, fetchAuthKeys, etc.)
unchanged.
Summary
auth_methodanduser_pathTesting
go test ./internal/auditlog ./internal/server ./internal/adminnode --test internal/admin/dashboard/static/js/modules/auth-keys.test.js internal/admin/dashboard/static/js/modules/timestamp-format.test.js internal/admin/dashboard/static/js/modules/dashboard-layout.test.js internal/admin/dashboard/static/js/modules/execution-plans.test.js internal/admin/dashboard/static/js/modules/execution-plans-layout.test.jsSummary by CodeRabbit
New Features
Tests