Show your API token on the profile#448
Conversation
Every user already gets an API token on creation (used by the Bearer token authenticator on /api routes), but there was no way to see or rotate it without querying the database. Backend: - Add GET /api/user/token returning the current user's token, generating one on the fly for users that predate token creation. - Add POST /api/user/token/regenerate which rotates the token in place via the new ApiTokensTable::regenerateApiToken() and returns the new value, so the frontend state is updated in the same request. Frontend: - New 'Your API Token' section on the Profile view with a masked token display plus Show/Hide, Copy, and Regenerate buttons. Regenerating swaps the displayed token to the fresh value from the response.
Address Cursor Bugbot findings on PR #448: - Add the sentry-mask class to the token display so the plaintext token never ends up in Sentry Session Replay recordings. Replay is configured with maskAllText: false, and .sentry-mask is always masked regardless of that setting. - Turn Regenerate into a two-step confirmation: first click arms the button ('Really? Old token stops working!'), second click rotates. Prevents a misclick from instantly invalidating the old token for existing API clients and mobile sessions. Also add dark-mode variants to the Show/Hide and Copy buttons, which was flagged in the earlier code review.
Address the second round of bot findings on PR #448: - Add a unique index on api_tokens.user_id (with a dedupe of any existing duplicates, keeping the most recently used token). Without the constraint, concurrent requests to the token endpoint could insert multiple rows for one user, and regenerating only rotated the first row — leaving extra tokens silently valid. Flagged by Cursor Bugbot; also matches an earlier code-review finding. - Reset tokenVisible after regenerating so the fresh token is not immediately revealed if the old one was showing. Flagged by Seer.
| try { | ||
| await navigator.clipboard.writeText(this.apiToken) | ||
| this.copied = true | ||
| setTimeout(() => this.copied = false, 2000) | ||
| } catch (error) { | ||
| console.log(error) | ||
| } |
There was a problem hiding this comment.
Bug: The copyToken function can copy an empty API token if the user clicks "Copy" before the fetchToken async call completes, leading to misleading "Copied!" feedback.
Severity: MEDIUM
Suggested Fix
Disable the "Copy" button until the apiToken has been successfully fetched and is no longer an empty string. This can be achieved by adding a :disabled="!apiToken" attribute to the button in the template. Alternatively, introduce a loading state to prevent interaction until the token is available.
Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.
Location: frontend/src/views/Profile.vue#L182-L188
Potential issue: The `copyToken` function copies `this.apiToken` to the clipboard. This
token is initialized as an empty string and populated by the asynchronous `fetchToken()`
call in the `mounted` hook. If a user clicks the "Copy" button before `fetchToken()`
resolves, which is possible on slow networks, an empty string is copied. The UI then
incorrectly displays a "Copied!" message, misleading the user into thinking they have
successfully copied the API token when their clipboard is empty.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 85525c8. Configure here.

Everyone already has an API token, but there's no way to actually see it without digging in the database.
Now the profile shows your token — so you can hit stuff like
/api/leaderboardfrom wherever — plus a regenerate button in case you leak it in a screenshot 🥔