feat: bot API token management page
The account page pointed users at a "bot-token list" that didn't exist: there was no GET endpoint, no UI route, and the client type lacked expiry. Add GET /v1/auth/tokens (caller-scoped, token_hash never serialised), extend the auth client with listTokens/expires_at and createToken expiry, and add a /profile/tokens page to list, create (revealing the raw bearer once), and revoke tokens. Link the account-page copy to it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,7 @@ import {
|
||||
changePassword,
|
||||
createToken,
|
||||
deleteToken,
|
||||
listTokens,
|
||||
getAuthConfig
|
||||
} from './auth';
|
||||
|
||||
@@ -170,6 +171,50 @@ describe('auth api client', () => {
|
||||
expect(url).toMatch(/\/v1\/auth\/tokens$/);
|
||||
});
|
||||
|
||||
it('createToken forwards expires_in_days when provided', async () => {
|
||||
fetchSpy.mockResolvedValueOnce(
|
||||
ok(
|
||||
{
|
||||
id: 't2',
|
||||
user_id: 'user-1',
|
||||
name: 'expiring',
|
||||
created_at: '2026-01-01T00:00:00Z',
|
||||
last_used_at: null,
|
||||
expires_at: '2026-02-01T00:00:00Z',
|
||||
bearer: 'raw'
|
||||
},
|
||||
201
|
||||
)
|
||||
);
|
||||
await createToken('expiring', 30);
|
||||
const init = fetchSpy.mock.calls[0][1] as RequestInit;
|
||||
expect(JSON.parse(init.body as string)).toEqual({ name: 'expiring', expires_in_days: 30 });
|
||||
});
|
||||
|
||||
it('listTokens GETs /v1/auth/tokens and unwraps items', async () => {
|
||||
fetchSpy.mockResolvedValueOnce(
|
||||
ok({
|
||||
items: [
|
||||
{
|
||||
id: 't1',
|
||||
user_id: 'user-1',
|
||||
name: 'ci-bot',
|
||||
created_at: '2026-01-01T00:00:00Z',
|
||||
last_used_at: null,
|
||||
expires_at: null
|
||||
}
|
||||
]
|
||||
})
|
||||
);
|
||||
const tokens = await listTokens();
|
||||
expect(tokens).toHaveLength(1);
|
||||
expect(tokens[0].name).toBe('ci-bot');
|
||||
const url = fetchSpy.mock.calls[0][0] as string;
|
||||
expect(url).toMatch(/\/v1\/auth\/tokens$/);
|
||||
const init = fetchSpy.mock.calls[0][1] as RequestInit;
|
||||
expect(init?.method ?? 'GET').toBe('GET');
|
||||
});
|
||||
|
||||
it('getAuthConfig GETs /v1/auth/config and parses the flag', async () => {
|
||||
fetchSpy.mockResolvedValueOnce(ok({ self_register_enabled: false }));
|
||||
const cfg = await getAuthConfig();
|
||||
|
||||
Reference in New Issue
Block a user