Skip to content

Commit 33f17fd

Browse files
committed
frontend/tokens: Dont require name for token
fix #230
1 parent b696910 commit 33f17fd

File tree

2 files changed

+123
-3
lines changed

2 files changed

+123
-3
lines changed

frontend/src/pages/Tokens.tsx

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,24 @@ export function Tokens() {
133133
e.preventDefault();
134134
try {
135135
await sodium.ready;
136-
const encryptedTokenName = sodium.crypto_box_seal(tokenName, pub_key as Uint8Array);
136+
// Generate automatic name if empty
137+
let finalTokenName = tokenName.trim();
138+
if (finalTokenName === "") {
139+
// Find the highest existing "Token-N" number
140+
let maxTokenNumber = 0;
141+
const tokenPattern = /^Token-(\d+)$/;
142+
for (const token of tokens) {
143+
const match = token.name.match(tokenPattern);
144+
if (match) {
145+
const num = parseInt(match[1], 10);
146+
if (num > maxTokenNumber) {
147+
maxTokenNumber = num;
148+
}
149+
}
150+
}
151+
finalTokenName = `Token-${maxTokenNumber + 1}`;
152+
}
153+
const encryptedTokenName = sodium.crypto_box_seal(finalTokenName, pub_key as Uint8Array);
137154
const { data, response, error } = await fetchClient.POST('/user/create_authorization_token', {
138155
body: { use_once: useOnce, name: Base64.fromUint8Array(encryptedTokenName) },
139156
credentials: 'same-origin'
@@ -146,7 +163,7 @@ export function Tokens() {
146163
token: await buildToken(user, data),
147164
use_once: data.use_once,
148165
id: data.id,
149-
name: tokenName,
166+
name: finalTokenName,
150167
createdAt: new Date(data.created_at * 1000),
151168
lastUsedAt: data.last_used_at ? new Date(data.last_used_at * 1000) : null,
152169
};
@@ -212,9 +229,11 @@ export function Tokens() {
212229
type="text"
213230
placeholder={t("tokens.name_placeholder")}
214231
value={tokenName}
215-
required
216232
onChange={(e) => setTokenName((e.target as HTMLInputElement).value)}
217233
/>
234+
<Form.Text className="text-muted small">
235+
{t("tokens.name_auto_generate")}
236+
</Form.Text>
218237
</Form.Group>
219238
<div className="d-flex align-items-center justify-content-between">
220239
<Form.Check
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { render, screen, waitFor } from '@testing-library/preact';
2+
import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
3+
import { Tokens } from '../Tokens';
4+
5+
/**
6+
* Tests for the Tokens component
7+
*
8+
* This component manages authorization tokens for the application.
9+
* It handles token creation, display, and deletion.
10+
*/
11+
describe('Tokens Component', () => {
12+
const mockUserData = {
13+
id: 'user-123',
14+
name: 'John Doe',
15+
16+
has_old_charger: false,
17+
};
18+
19+
let fetchClient: typeof import('../../utils').fetchClient;
20+
let showAlert: Mock;
21+
22+
beforeEach(async () => {
23+
vi.clearAllMocks();
24+
25+
const utils = await import('../../utils');
26+
fetchClient = utils.fetchClient;
27+
28+
const alertModule = await import('../../components/Alert');
29+
showAlert = alertModule.showAlert as unknown as Mock;
30+
});
31+
32+
it('renders loading spinner initially', () => {
33+
(fetchClient.GET as unknown as Mock).mockImplementation(() =>
34+
new Promise(() => {}) // Never resolves
35+
);
36+
37+
render(<Tokens />);
38+
expect(screen.getByText('Loading...')).toBeTruthy();
39+
});
40+
41+
it('renders component after successful data fetch', async () => {
42+
(fetchClient.GET as unknown as Mock).mockImplementation((path: string) => {
43+
if (path === '/user/me') {
44+
return Promise.resolve({
45+
data: mockUserData,
46+
error: null,
47+
response: { status: 200 },
48+
});
49+
}
50+
if (path === '/user/get_authorization_tokens') {
51+
return Promise.resolve({
52+
data: { tokens: [] },
53+
error: null,
54+
response: { status: 200 },
55+
});
56+
}
57+
return Promise.resolve({ data: null, error: 'Not found', response: { status: 404 } });
58+
});
59+
60+
render(<Tokens />);
61+
62+
await waitFor(() => {
63+
expect(screen.queryByText('Loading...')).toBeNull();
64+
}, { timeout: 2000 });
65+
66+
expect(screen.getByText('tokens.create_token')).toBeTruthy();
67+
});
68+
69+
it('shows error when user fetch fails', async () => {
70+
(fetchClient.GET as unknown as Mock).mockImplementation((path: string) => {
71+
if (path === '/user/me') {
72+
return Promise.resolve({ data: null, error: 'Error', response: { status: 400 } });
73+
}
74+
return Promise.resolve({ data: { tokens: [] }, error: null, response: { status: 200 } });
75+
});
76+
77+
render(<Tokens />);
78+
79+
await waitFor(() => {
80+
expect(showAlert).toHaveBeenCalledWith('tokens.fetch_user_failed', 'danger');
81+
}, { timeout: 2000 });
82+
});
83+
84+
it('shows error when tokens fetch fails', async () => {
85+
(fetchClient.GET as unknown as Mock).mockImplementation((path: string) => {
86+
if (path === '/user/me') {
87+
return Promise.resolve({ data: mockUserData, error: null, response: { status: 200 } });
88+
}
89+
if (path === '/user/get_authorization_tokens') {
90+
return Promise.resolve({ data: null, error: 'Error', response: { status: 400 } });
91+
}
92+
return Promise.resolve({ data: null, error: 'Not found', response: { status: 404 } });
93+
});
94+
95+
render(<Tokens />);
96+
97+
await waitFor(() => {
98+
expect(showAlert).toHaveBeenCalledWith('tokens.fetch_tokens_failed', 'danger');
99+
}, { timeout: 2000 });
100+
});
101+
});

0 commit comments

Comments
 (0)