Conversation
Move api_key_hash/api_key_prefix from teams table into a new api_keys table with FK to team, enabling multiple concurrent keys per team. Add CRUD endpoints for key management (create/list/delete) and a migration that carries existing keys over. The frontend replaces the single regenerate-key button with a full key management dialog supporting both auto-generated and manually provided keys. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. 📝 WalkthroughWalkthroughThis PR refactors API key management from single-key-per-team (stored in Team model) to multi-key support via a dedicated ApiKey model. It introduces CRUD endpoints (list, create, delete), new schemas for API key responses, a database migration, and updates frontend components to display and manage multiple API keys. Changes
Sequence DiagramsequenceDiagram
participant Admin as Admin User
participant API as Admin API
participant DB as Database
participant Auth as Auth Validator
rect rgba(0, 100, 200, 0.5)
Note over Admin,DB: Team Creation Flow (New)
Admin->>API: POST /teams (TeamCreate)
API->>DB: Create Team
API->>API: generate_api_key()
API->>DB: Store in api_keys table
API-->>Admin: TeamCreateResponse with api_keys[0] + secret
end
rect rgba(100, 0, 200, 0.5)
Note over Admin,DB: API Key Validation (Updated)
Admin->>Auth: API Request (x-api-key header)
Auth->>DB: Query ApiKey by api_key_hash
DB-->>Auth: Return Team via FK relation
Auth-->>Admin: Proceed with request
end
rect rgba(0, 200, 100, 0.5)
Note over Admin,DB: API Key Management (New)
Admin->>API: GET /teams/{team_id}/api-keys
API->>DB: Query ApiKey by team_id
DB-->>API: list[ApiKeyResponse]
API-->>Admin: Display key list
Admin->>API: POST /teams/{team_id}/api-keys (create)
API->>API: generate_api_key() or hash user key
API->>DB: Store in api_keys
API-->>Admin: ApiKeyWithSecret (includes secret)
Admin->>API: DELETE /teams/{team_id}/api-keys/{key_id}
API->>DB: Delete ApiKey record
DB-->>API: Success
API-->>Admin: Confirm deletion
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes 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 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 |
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (11)
frontend/src/views/admin/Teams.vue (3)
18-25: Minor: comma separator is inside the<code>tag.The trailing
,on line 22 renders inside the<code>element, so the comma inherits monospace/code styling. Consider moving the separator outside the<code>tag for cleaner visual output.Proposed fix
- <span v-else> - <code v-for="(key, i) in item.api_keys" :key="key.id"> - {{ key.api_key_prefix }}...{{ i < item.api_keys.length - 1 ? ', ' : '' }} - </code> - </span> + <span v-else> + <template v-for="(key, i) in item.api_keys" :key="key.id"> + <code>{{ key.api_key_prefix }}...</code>{{ i < item.api_keys.length - 1 ? ', ' : '' }} + </template> + </span>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/views/admin/Teams.vue` around lines 18 - 25, The comma separator is being rendered inside the <code> for the API key list in the template `#item.api_keys`; update the template so the <code> element contains only the key text (key.api_key_prefix) and render the separator (', ') outside the <code> element (e.g. conditionally render a text node or span after the <code> when i < item.api_keys.length - 1) to keep the comma styling consistent and avoid monospace formatting leaking into the separator.
355-357:copyKeyhas no user feedback or error handling.
navigator.clipboard.writeTextcan fail (e.g., in non-secure contexts) and provides no visual confirmation to the user on success. A brief snackbar/toast or try-catch would improve UX.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/views/admin/Teams.vue` around lines 355 - 357, Update the copyKey function to await navigator.clipboard.writeText(newApiKey.value) inside a try/catch, and on success call the component's user-feedback routine (e.g., this.$toast.success, showSnackbar, or emit a "notify" event) to show a brief confirmation; on failure catch the error, show an error notification (with the error message) and optionally fall back to a safe clipboard approach (selecting the text and using document.execCommand('copy')) when navigator.clipboard is unavailable. Ensure you reference and use the existing copyKey function and newApiKey.value when implementing the changes.
408-417: Key revocation lacks a confirmation prompt.
revokeKeyimmediately deletes the API key on click with no confirmation. Since this is a destructive and irreversible action (active integrations would break), consider adding a confirmation dialog similar to the team delete flow.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/views/admin/Teams.vue` around lines 408 - 417, The revokeKey function currently deletes an API key immediately; wrap its destructive operation in a user confirmation step: before calling api.delete in revokeKey, open the same confirmation dialog/modal used by the team delete flow (or use a simple window.confirm) and only proceed with the API call, teamKeys update, and fetchTeams() when the user confirms; if the user cancels, return without making any changes. Ensure you reference revokeKey, selectedTeam, teamKeys, and fetchTeams so the dialog gating is applied exactly where the deletion is triggered.backend/migrations/004_api_keys_table.sql (2)
17-23: Redundant index onapi_key_hash.Line 17 declares
api_key_hash VARCHAR(255) NOT NULL UNIQUE, which in PostgreSQL implicitly creates a unique index. The explicitCREATE INDEX idx_api_keys_hashon line 23 adds a second, non-unique index on the same column. It's not harmful but wastes disk space and write overhead.Remove the redundant index
CREATE INDEX idx_api_keys_team_id ON api_keys(team_id); - CREATE INDEX idx_api_keys_hash ON api_keys(api_key_hash);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/migrations/004_api_keys_table.sql` around lines 17 - 23, The schema defines api_key_hash as UNIQUE on the api_keys table, which already creates a unique index, so remove the redundant explicit CREATE INDEX idx_api_keys_hash ON api_keys(api_key_hash); statement and keep the UNIQUE constraint (api_key_hash VARCHAR(255) NOT NULL UNIQUE) intact to avoid duplicate indexing and unnecessary disk/write overhead.
4-10: Partial-failure edge case in idempotency guard.The guard checks only for table existence. If the migration partially completes (e.g., table created but
INSERTorALTER TABLE DROP COLUMNfails), a re-run will skip everything, leaving the migration in an inconsistent state. This is acceptable if the migration runner wraps the entire script in a transaction (which is typical), but worth confirming.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/migrations/004_api_keys_table.sql` around lines 4 - 10, The idempotency guard only checks for the existence of the api_keys table which can hide partial failures (table created but later INSERTs/ALTERs failed); update the migration to ensure atomicity by wrapping the entire migration in a transaction (BEGIN/COMMIT) or enhance the guard to check for a definitive migration marker such as specific columns, constraints or a version row (e.g., verify required columns/constraints exist on api_keys or a migration_version table entry) before skipping; refer to the api_keys table and the ALTER/INSERT steps in this file when implementing the transaction wrapper or the more specific existence checks.backend/app/schemas/team.py (2)
50-58:TeamCreateResponseduplicatesTeamResponsefields.
TeamCreateResponserepeats all fields fromTeamResponseplus addsapi_key. Inheriting fromTeamResponsewould reduce duplication and keep the two in sync.Inherit from TeamResponse
-class TeamCreateResponse(BaseModel): +class TeamCreateResponse(TeamResponse): """Response when creating a team - includes the first API key with secret.""" - id: UUID - name: str - api_keys: list[ApiKeyResponse] - retention_days: int | None - created_at: datetime - updated_at: datetime api_key: str🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/app/schemas/team.py` around lines 50 - 58, TeamCreateResponse duplicates all fields from TeamResponse; change TeamCreateResponse to inherit from TeamResponse (e.g., class TeamCreateResponse(TeamResponse):) and remove the duplicated attributes (id, name, api_keys, retention_days, created_at, updated_at) leaving only the additional api_key field so the response extends TeamResponse and stays in sync with it; ensure ApiKeyResponse and typing for api_key remain correct.
33-35: Consider validating manually provided API keys.
ApiKeyCreate.api_keyaccepts any string with no minimum length or character constraints. A user could submit a trivially short or weak key (e.g.,"a"), which would be stored and used for authentication. Adding a minimum length validator would mitigate this.Add minimum length validation
+from pydantic import BaseModel, field_validator + class ApiKeyCreate(BaseModel): label: str = "" api_key: str | None = None + + `@field_validator`("api_key") + `@classmethod` + def validate_api_key(cls, v: str | None) -> str | None: + if v is not None and len(v) < 20: + raise ValueError("API key must be at least 20 characters long") + return v🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/app/schemas/team.py` around lines 33 - 35, ApiKeyCreate.api_key currently allows any string; add a minimum-length validation so manually provided API keys are not trivially short — modify the ApiKeyCreate model to enforce a min length (e.g., 32) for api_key either by changing the field type to a pydantic constrained string (constr(min_length=32)) or by adding a `@validator`("api_key") that returns the value only if value is None or len(value) >= 32 and raises a ValueError otherwise; update the ApiKeyCreate.api_key annotation accordingly and include a clear error message in the raised exception.backend/app/models/api_key.py (1)
32-39: Consider adding a return type annotation.
get_team_by_api_keyreturnsOptional[Team]but the signature lacks a type hint. Adding it would improve IDE support and static analysis.Add return type hint
+ from __future__ import annotations + from typing import TYPE_CHECKING + if TYPE_CHECKING: + from app.models.team import Team + `@classmethod` - async def get_team_by_api_key(cls, api_key: str): + async def get_team_by_api_key(cls, api_key: str) -> "Team | None": """Find a team by API key."""🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/app/models/api_key.py` around lines 32 - 39, Add an explicit return type to the get_team_by_api_key method: annotate it as -> Optional[Team]. Import Optional from typing (or typing.Optional) and ensure Team is imported or referenced correctly (e.g., from .team import Team or using a forward reference string "Team" if necessary) and update the signature of async def get_team_by_api_key(cls, api_key: str) to include the new return type.backend/app/api/admin.py (3)
98-125:_team_responsehelper is unused inlist_teams.
list_teamsbuildsTeamResponseinline (withprefetch_related, which is the right call for batch operations), whileget_teamandupdate_teamuse_team_responsewhich issues a separate query. The helper is fine for single-team endpoints, but consider either removing it and inlining everywhere, or usingprefetch_relatedinside_team_responsetoo for consistency.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/app/api/admin.py` around lines 98 - 125, The helper _team_response is unused by list_teams and currently always issues its own ApiKey query (causing inconsistency/N+1 risk); modify _team_response(team: Team) so it first checks if the Team instance already has a prefetched .api_keys attribute and uses those keys to build ApiKeyResponse objects, and only falls back to awaiting ApiKey.filter(team=team).all() when .api_keys is missing; keep list_teams as-is (it prefetches) and ensure get_team/update_team can continue to call _team_response unchanged.
254-262: Deleting the last API key leaves a team with no key.There's no guard preventing deletion of a team's only remaining API key, which would silently break all API-key-authenticated ingestion for that team. Consider either warning or preventing this.
Guard against deleting the last key
key = await ApiKey.filter(id=key_id, team_id=team_id).first() if key is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="API key not found") + remaining = await ApiKey.filter(team_id=team_id).count() + if remaining <= 1: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Cannot delete the last API key for a team", + ) + await key.delete() return {"message": "API key revoked"}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/app/api/admin.py` around lines 254 - 262, In delete_api_key, prevent revoking the team's last API key by checking ApiKey entries for the given team before deletion: use ApiKey.filter(team_id=team_id).count() (or fetch all and len) and if the count is 1 raise an HTTPException (400/409) with a clear message instead of deleting; otherwise proceed to delete the key fetched by ApiKey.filter(id=key_id, team_id=team_id).first() as currently implemented.
222-227: Prefix for manually provided keys leaks up to 10 characters in plaintext.For auto-generated keys the stored
api_key_prefixis the non-secretsl_XXXXportion, but for manually provided keys it'sfull_key[:10], which is the beginning of the actual secret. If the key has low entropy or a recognizable structure, this prefix narrows the search space. Consider documenting this trade-off or letting the user supply their own prefix.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/app/api/admin.py` around lines 222 - 227, The current branch sets prefix = full_key[:10] for manually provided keys which leaks up to 10 plaintext characters; change this to either accept a user-supplied non-secret prefix (check for data.api_key_prefix) or derive a non-revealing prefix from the key hash (e.g., use ApiKey.hash_api_key(full_key) and take a truncated portion of the hex hash) and store that in api_key_prefix instead of slicing the raw key; update the logic around full_key, ApiKey.hash_api_key and prefix to prefer data.api_key_prefix, otherwise compute a hash-derived prefix so no plaintext secret characters are stored.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@backend/app/api/admin.py`:
- Around line 215-251: The create_api_key endpoint currently does a pre-check
for duplicate api_key_hash but can still hit a UNIQUE constraint race; wrap the
ApiKey.create call in a try/except that catches the database IntegrityError
(from your ORM/DB driver), and on IntegrityError translate it into a
HTTPException with status_code 400 and a clear message like "This API key
already exists"; keep the existing pre-check but ensure the except path handles
the concurrent-insert case for ApiKey.create so duplicate hash insertion doesn't
produce a raw 500.
In `@frontend/src/stores/teams.ts`:
- Around line 45-60: The Teams store defines getApiKeys, createApiKey, and
deleteApiKey but Teams.vue still calls api.get/post/delete directly (in
openKeysDialog, addKey, revokeKey); refactor Teams.vue to import and call these
store functions instead of duplicating the HTTP calls: replace the direct
api.get(`/admin/teams/${teamId}/api-keys`) with getApiKeys(teamId), the api.post
call with createApiKey(teamId, label, apiKey), and the api.delete call with
deleteApiKey(teamId, keyId); keep existing async/await, error handling, and
response handling in openKeysDialog/addKey/revokeKey so returned values and
types (ApiKey / ApiKeyWithSecret) are used unchanged.
In `@frontend/src/views/admin/Teams.vue`:
- Around line 378-406: In addKey(), prevent the silent-generation bug by adding
two small checks: if addKeyMode.value === 'manual' and newKeyForm.api_key is
empty, abort (set addingKey false and return or surface validation) so we don't
submit an empty API key; and after the POST, show the secret dialog whenever the
backend returns a generated secret (i.e. if response.data.api_key exists) by
assigning newApiKey.value = response.data.api_key and setting keyDialog.value =
true (instead of only checking addKeyMode === 'generate'). Update the addKey
function logic around the request and response handling to enforce the
manual-mode validation and to show the secret when response.data.api_key is
present.
---
Nitpick comments:
In `@backend/app/api/admin.py`:
- Around line 98-125: The helper _team_response is unused by list_teams and
currently always issues its own ApiKey query (causing inconsistency/N+1 risk);
modify _team_response(team: Team) so it first checks if the Team instance
already has a prefetched .api_keys attribute and uses those keys to build
ApiKeyResponse objects, and only falls back to awaiting
ApiKey.filter(team=team).all() when .api_keys is missing; keep list_teams as-is
(it prefetches) and ensure get_team/update_team can continue to call
_team_response unchanged.
- Around line 254-262: In delete_api_key, prevent revoking the team's last API
key by checking ApiKey entries for the given team before deletion: use
ApiKey.filter(team_id=team_id).count() (or fetch all and len) and if the count
is 1 raise an HTTPException (400/409) with a clear message instead of deleting;
otherwise proceed to delete the key fetched by ApiKey.filter(id=key_id,
team_id=team_id).first() as currently implemented.
- Around line 222-227: The current branch sets prefix = full_key[:10] for
manually provided keys which leaks up to 10 plaintext characters; change this to
either accept a user-supplied non-secret prefix (check for data.api_key_prefix)
or derive a non-revealing prefix from the key hash (e.g., use
ApiKey.hash_api_key(full_key) and take a truncated portion of the hex hash) and
store that in api_key_prefix instead of slicing the raw key; update the logic
around full_key, ApiKey.hash_api_key and prefix to prefer data.api_key_prefix,
otherwise compute a hash-derived prefix so no plaintext secret characters are
stored.
In `@backend/app/models/api_key.py`:
- Around line 32-39: Add an explicit return type to the get_team_by_api_key
method: annotate it as -> Optional[Team]. Import Optional from typing (or
typing.Optional) and ensure Team is imported or referenced correctly (e.g., from
.team import Team or using a forward reference string "Team" if necessary) and
update the signature of async def get_team_by_api_key(cls, api_key: str) to
include the new return type.
In `@backend/app/schemas/team.py`:
- Around line 50-58: TeamCreateResponse duplicates all fields from TeamResponse;
change TeamCreateResponse to inherit from TeamResponse (e.g., class
TeamCreateResponse(TeamResponse):) and remove the duplicated attributes (id,
name, api_keys, retention_days, created_at, updated_at) leaving only the
additional api_key field so the response extends TeamResponse and stays in sync
with it; ensure ApiKeyResponse and typing for api_key remain correct.
- Around line 33-35: ApiKeyCreate.api_key currently allows any string; add a
minimum-length validation so manually provided API keys are not trivially short
— modify the ApiKeyCreate model to enforce a min length (e.g., 32) for api_key
either by changing the field type to a pydantic constrained string
(constr(min_length=32)) or by adding a `@validator`("api_key") that returns the
value only if value is None or len(value) >= 32 and raises a ValueError
otherwise; update the ApiKeyCreate.api_key annotation accordingly and include a
clear error message in the raised exception.
In `@backend/migrations/004_api_keys_table.sql`:
- Around line 17-23: The schema defines api_key_hash as UNIQUE on the api_keys
table, which already creates a unique index, so remove the redundant explicit
CREATE INDEX idx_api_keys_hash ON api_keys(api_key_hash); statement and keep the
UNIQUE constraint (api_key_hash VARCHAR(255) NOT NULL UNIQUE) intact to avoid
duplicate indexing and unnecessary disk/write overhead.
- Around line 4-10: The idempotency guard only checks for the existence of the
api_keys table which can hide partial failures (table created but later
INSERTs/ALTERs failed); update the migration to ensure atomicity by wrapping the
entire migration in a transaction (BEGIN/COMMIT) or enhance the guard to check
for a definitive migration marker such as specific columns, constraints or a
version row (e.g., verify required columns/constraints exist on api_keys or a
migration_version table entry) before skipping; refer to the api_keys table and
the ALTER/INSERT steps in this file when implementing the transaction wrapper or
the more specific existence checks.
In `@frontend/src/views/admin/Teams.vue`:
- Around line 18-25: The comma separator is being rendered inside the <code> for
the API key list in the template `#item.api_keys`; update the template so the
<code> element contains only the key text (key.api_key_prefix) and render the
separator (', ') outside the <code> element (e.g. conditionally render a text
node or span after the <code> when i < item.api_keys.length - 1) to keep the
comma styling consistent and avoid monospace formatting leaking into the
separator.
- Around line 355-357: Update the copyKey function to await
navigator.clipboard.writeText(newApiKey.value) inside a try/catch, and on
success call the component's user-feedback routine (e.g., this.$toast.success,
showSnackbar, or emit a "notify" event) to show a brief confirmation; on failure
catch the error, show an error notification (with the error message) and
optionally fall back to a safe clipboard approach (selecting the text and using
document.execCommand('copy')) when navigator.clipboard is unavailable. Ensure
you reference and use the existing copyKey function and newApiKey.value when
implementing the changes.
- Around line 408-417: The revokeKey function currently deletes an API key
immediately; wrap its destructive operation in a user confirmation step: before
calling api.delete in revokeKey, open the same confirmation dialog/modal used by
the team delete flow (or use a simple window.confirm) and only proceed with the
API call, teamKeys update, and fetchTeams() when the user confirms; if the user
cancels, return without making any changes. Ensure you reference revokeKey,
selectedTeam, teamKeys, and fetchTeams so the dialog gating is applied exactly
where the deletion is triggered.
| @router.post("/teams/{team_id}/api-keys", response_model=ApiKeyWithSecret, status_code=status.HTTP_201_CREATED) | ||
| async def create_api_key(admin: AdminUser, team_id: UUID, data: ApiKeyCreate): | ||
| """Create a new API key for a team. If api_key is provided, use it; otherwise auto-generate.""" | ||
| team = await Team.filter(id=team_id).first() | ||
| if team is None: | ||
| raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Team not found") | ||
|
|
||
| if data.api_key: | ||
| # Manually provided key | ||
| full_key = data.api_key | ||
| key_hash = ApiKey.hash_api_key(full_key) | ||
| # Derive prefix from the key (first 10 chars or less) | ||
| prefix = full_key[:10] if len(full_key) > 10 else full_key | ||
|
|
||
| # Check for duplicate hash | ||
| existing = await ApiKey.filter(api_key_hash=key_hash).first() | ||
| if existing: | ||
| raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="This API key already exists") | ||
| else: | ||
| # Auto-generate | ||
| full_key, key_hash, prefix = ApiKey.generate_api_key() | ||
|
|
||
| key_obj = await ApiKey.create( | ||
| team=team, | ||
| label=data.label, | ||
| api_key_hash=key_hash, | ||
| api_key_prefix=prefix, | ||
| ) | ||
|
|
||
| return ApiKeyWithSecret( | ||
| id=key_obj.id, | ||
| team_id=team_id, | ||
| label=key_obj.label, | ||
| api_key_prefix=key_obj.api_key_prefix, | ||
| created_at=key_obj.created_at, | ||
| api_key=full_key, | ||
| ) |
There was a problem hiding this comment.
Manual key creation could hit an unhandled IntegrityError on concurrent duplicate.
Lines 230-232 pre-check for a duplicate hash, but there's a TOCTOU window — a concurrent request with the same key could pass the check and then fail on the UNIQUE constraint at insert time, producing a raw 500. Consider catching IntegrityError around the create call.
Wrap create in IntegrityError handler
+ from tortoise.exceptions import IntegrityError
+
+ try:
key_obj = await ApiKey.create(
team=team,
label=data.label,
api_key_hash=key_hash,
api_key_prefix=prefix,
)
+ except IntegrityError:
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail="This API key already exists",
+ )📝 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.
| @router.post("/teams/{team_id}/api-keys", response_model=ApiKeyWithSecret, status_code=status.HTTP_201_CREATED) | |
| async def create_api_key(admin: AdminUser, team_id: UUID, data: ApiKeyCreate): | |
| """Create a new API key for a team. If api_key is provided, use it; otherwise auto-generate.""" | |
| team = await Team.filter(id=team_id).first() | |
| if team is None: | |
| raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Team not found") | |
| if data.api_key: | |
| # Manually provided key | |
| full_key = data.api_key | |
| key_hash = ApiKey.hash_api_key(full_key) | |
| # Derive prefix from the key (first 10 chars or less) | |
| prefix = full_key[:10] if len(full_key) > 10 else full_key | |
| # Check for duplicate hash | |
| existing = await ApiKey.filter(api_key_hash=key_hash).first() | |
| if existing: | |
| raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="This API key already exists") | |
| else: | |
| # Auto-generate | |
| full_key, key_hash, prefix = ApiKey.generate_api_key() | |
| key_obj = await ApiKey.create( | |
| team=team, | |
| label=data.label, | |
| api_key_hash=key_hash, | |
| api_key_prefix=prefix, | |
| ) | |
| return ApiKeyWithSecret( | |
| id=key_obj.id, | |
| team_id=team_id, | |
| label=key_obj.label, | |
| api_key_prefix=key_obj.api_key_prefix, | |
| created_at=key_obj.created_at, | |
| api_key=full_key, | |
| ) | |
| `@router.post`("/teams/{team_id}/api-keys", response_model=ApiKeyWithSecret, status_code=status.HTTP_201_CREATED) | |
| async def create_api_key(admin: AdminUser, team_id: UUID, data: ApiKeyCreate): | |
| """Create a new API key for a team. If api_key is provided, use it; otherwise auto-generate.""" | |
| team = await Team.filter(id=team_id).first() | |
| if team is None: | |
| raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Team not found") | |
| if data.api_key: | |
| # Manually provided key | |
| full_key = data.api_key | |
| key_hash = ApiKey.hash_api_key(full_key) | |
| # Derive prefix from the key (first 10 chars or less) | |
| prefix = full_key[:10] if len(full_key) > 10 else full_key | |
| # Check for duplicate hash | |
| existing = await ApiKey.filter(api_key_hash=key_hash).first() | |
| if existing: | |
| raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="This API key already exists") | |
| else: | |
| # Auto-generate | |
| full_key, key_hash, prefix = ApiKey.generate_api_key() | |
| from tortoise.exceptions import IntegrityError | |
| try: | |
| key_obj = await ApiKey.create( | |
| team=team, | |
| label=data.label, | |
| api_key_hash=key_hash, | |
| api_key_prefix=prefix, | |
| ) | |
| except IntegrityError: | |
| raise HTTPException( | |
| status_code=status.HTTP_400_BAD_REQUEST, | |
| detail="This API key already exists", | |
| ) | |
| return ApiKeyWithSecret( | |
| id=key_obj.id, | |
| team_id=team_id, | |
| label=key_obj.label, | |
| api_key_prefix=key_obj.api_key_prefix, | |
| created_at=key_obj.created_at, | |
| api_key=full_key, | |
| ) |
🧰 Tools
🪛 Ruff (0.15.1)
[warning] 216-216: Unused function argument: admin
(ARG001)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@backend/app/api/admin.py` around lines 215 - 251, The create_api_key endpoint
currently does a pre-check for duplicate api_key_hash but can still hit a UNIQUE
constraint race; wrap the ApiKey.create call in a try/except that catches the
database IntegrityError (from your ORM/DB driver), and on IntegrityError
translate it into a HTTPException with status_code 400 and a clear message like
"This API key already exists"; keep the existing pre-check but ensure the except
path handles the concurrent-insert case for ApiKey.create so duplicate hash
insertion doesn't produce a raw 500.
Use teamsStore.getApiKeys/createApiKey/deleteApiKey instead of direct api calls. Add empty-key guard for manual mode and show the secret dialog based on backend response rather than UI mode. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move api_key_hash/api_key_prefix from teams table into a new api_keys table with FK to team, enabling multiple concurrent keys per team. Add CRUD endpoints for key management (create/list/delete) and a migration that carries existing keys over. The frontend replaces the single regenerate-key button with a full key management dialog supporting both auto-generated and manually provided keys.
Summary by CodeRabbit
Release Notes