Skip to content

Commit d367a4a

Browse files
committed
frontend/tokens: adapt to new token api.
Change ui to allow setting a and displaying the name and show the creation and last usage date.
1 parent b4e4c61 commit d367a4a

File tree

4 files changed

+93
-37
lines changed

4 files changed

+93
-37
lines changed

frontend/src/locales/de.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,11 @@ export const de ={
135135
"copy_failed": "Token konnte nicht kopiert werden",
136136
"create_token": "Autorisierungstoken erstellen",
137137
"use_once": "Einmal verwendbar",
138+
"name": "Name",
139+
"name_placeholder": "Name",
140+
"created": "Erstellt",
141+
"last_used": "Zuletzt verwendet",
142+
"never_used": "Nie verwendet",
138143
"create": "Token erstellen",
139144
"existing_tokens": "Vorhandene Token",
140145
"reusable": "Wiederverwendbar",

frontend/src/locales/en.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,11 @@ export const en = {
138138
"copy_failed": "Failed to copy token",
139139
"create_token": "Create authorization token",
140140
"use_once": "Use once",
141+
"name": "Name",
142+
"name_placeholder": "Name",
143+
"created": "Created",
144+
"last_used": "Last used",
145+
"never_used": "Never used",
141146
"create": "Create token",
142147
"existing_tokens": "Existing tokens",
143148
"reusable": "Reusable",

frontend/src/pages/tokens.tsx

Lines changed: 77 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import { useEffect, useState } from 'preact/hooks';
22
import { Button, Card, Container, Form, Spinner, InputGroup, Alert } from 'react-bootstrap';
3-
import { fetchClient, get_decrypted_secret, pub_key } from '../utils';
3+
import { fetchClient, get_decrypted_secret, pub_key, secret } from '../utils';
44
import { showAlert } from '../components/Alert';
55
import { Base64 } from 'js-base64';
66
import { encodeBase58Flickr } from '../base58';
77
import { useTranslation } from 'react-i18next';
88
import { Clipboard, Trash2 } from 'react-feather';
99
import { components } from '../schema';
1010
import { ArgonType, hash } from 'argon2-browser';
11+
import sodium from 'libsodium-wrappers';
1112

1213
async function buildToken(userData: components["schemas"]["UserInfo"], tokenData: components["schemas"]["GetAuthorizationTokensResponseSchema"]["tokens"][0]) {
1314
// Reserve a buffer with documented size
@@ -52,8 +53,12 @@ export function Tokens() {
5253
token: string,
5354
use_once: boolean,
5455
id: string,
56+
name: string,
57+
createdAt: Date,
58+
lastUsedAt: Date | null,
5559
}[]>([]);
5660
const [useOnce, setUseOnce] = useState(true);
61+
const [tokenName, setTokenName] = useState("");
5762
const [user, setUser] = useState<components["schemas"]["UserInfo"] | null>(null);
5863
const [loading, setLoading] = useState(true);
5964

@@ -84,17 +89,21 @@ export function Tokens() {
8489
}
8590

8691
// Process and set tokens
87-
const newTokens: {
88-
token: string,
89-
use_once: boolean,
90-
id: string,
91-
}[] = [];
92+
const newTokens: typeof tokens = [];
9293
for (const token of tokensData.tokens) {
9394
const newToken = await buildToken(userData, token);
95+
let tokenName = "";
96+
if (token.name.length !== 0) {
97+
const binaryName = Base64.toUint8Array(token.name);
98+
tokenName = new TextDecoder().decode(sodium.crypto_box_seal_open(binaryName, pub_key as Uint8Array, secret as Uint8Array));
99+
}
94100
newTokens.push({
95101
token: newToken,
96102
use_once: token.use_once,
97-
id: token.id
103+
id: token.id,
104+
name: tokenName,
105+
createdAt: new Date(token.created_at * 1000),
106+
lastUsedAt: token.last_used_at ? new Date(token.last_used_at * 1000) : null,
98107
});
99108
}
100109
setTokens(newTokens);
@@ -123,25 +132,27 @@ export function Tokens() {
123132
const handleCreateToken = async (e: SubmitEvent) => {
124133
e.preventDefault();
125134
try {
135+
await sodium.ready;
136+
const encryptedTokenName = sodium.crypto_box_seal(tokenName, pub_key as Uint8Array);
126137
const { data, response, error } = await fetchClient.POST('/user/create_authorization_token', {
127-
body: { use_once: useOnce },
138+
body: { use_once: useOnce, name: Base64.fromUint8Array(encryptedTokenName) },
128139
credentials: 'same-origin'
129140
});
130141
if (error || response.status !== 201 || !data || !user) {
131142
showAlert(t("tokens.create_token_failed"), "danger");
132143
return;
133144
}
134-
const newToken: {
135-
token: string,
136-
use_once: boolean,
137-
id: string,
138-
} = {
145+
const newToken: typeof tokens[0] = {
139146
token: await buildToken(user, data),
140147
use_once: data.use_once,
141-
id: data.id
148+
id: data.id,
149+
name: tokenName,
150+
createdAt: new Date(data.created_at * 1000),
151+
lastUsedAt: data.last_used_at ? new Date(data.last_used_at * 1000) : null,
142152
};
143153

144154
setTokens([...tokens, newToken]);
155+
setTokenName(""); // Clear the name field after successful creation
145156
} catch (err) {
146157
console.error(err);
147158
showAlert(t("tokens.unexpected_error"), "danger");
@@ -194,6 +205,16 @@ export function Tokens() {
194205
</Card.Header>
195206
<Card.Body>
196207
<Form onSubmit={handleCreateToken}>
208+
<Form.Group className="mb-3">
209+
<Form.Label>{t("tokens.name")}</Form.Label>
210+
<Form.Control
211+
type="text"
212+
placeholder={t("tokens.name_placeholder")}
213+
value={tokenName}
214+
required
215+
onChange={(e) => setTokenName((e.target as HTMLInputElement).value)}
216+
/>
217+
</Form.Group>
197218
<div className="d-flex align-items-center justify-content-between">
198219
<Form.Check
199220
type="switch"
@@ -220,42 +241,61 @@ export function Tokens() {
220241
</Card.Header>
221242
<Card.Body>
222243
{tokens.map((token, index) => (
223-
<>
224-
<InputGroup key={index} className={`token-group ${index !== tokens.length - 1 ? 'mb-3' : ''}`}>
225-
<Form.Control
226-
type="text"
227-
readOnly
228-
value={token.token}
229-
className="mb-2 mb-md-0 token-txt"
230-
/>
231-
<div className="d-flex flex-wrap gap-2 gap-md-0 mt-2 mt-md-0">
232-
<Button
233-
variant={token.use_once ? "success" : "warning"}
234-
disabled
235-
className="flex-grow-1 flex-md-grow-0"
236-
>
237-
{token.use_once ? t("tokens.use_once") : t("tokens.reusable")}
238-
</Button>
244+
<div key={index} className={`token-item ${index !== tokens.length - 1 ? 'mb-4' : ''}`}>
245+
<div className="d-flex justify-content-between align-items-start mb-2">
246+
<div>
247+
<h6 className="mb-1 fw-bold">{token.name}</h6>
248+
<small className="text-muted">
249+
{t("tokens.created")}: {token.createdAt.toLocaleDateString()} {token.createdAt.toLocaleTimeString()}
250+
</small>
251+
<br />
252+
<small className="text-muted">
253+
{t("tokens.last_used")}: {token.lastUsedAt ?
254+
`${token.lastUsedAt.toLocaleDateString()} ${token.lastUsedAt.toLocaleTimeString()}` :
255+
t("tokens.never_used")
256+
}
257+
</small>
258+
</div>
259+
<div className="d-flex gap-2">
260+
<Button
261+
variant={token.use_once ? "success" : "warning"}
262+
disabled
263+
size="sm"
264+
>
265+
{token.use_once ? t("tokens.use_once") : t("tokens.reusable")}
266+
</Button>
267+
</div>
268+
</div>
269+
<InputGroup className="mb-2">
270+
<Form.Control
271+
type="text"
272+
readOnly
273+
value={token.token}
274+
className="token-txt"
275+
/>
276+
</InputGroup>
277+
<div className="d-flex flex-wrap gap-2">
239278
<Button
240279
variant="secondary"
241-
className="flex-grow-1 flex-md-grow-0 d-flex align-items-center justify-content-center gap-2"
280+
size="sm"
281+
className="d-flex align-items-center gap-2"
242282
onClick={() => handleCopyToken(token.token)}
243283
>
244-
<Clipboard size={18} />
284+
<Clipboard size={16} />
245285
{t("tokens.copy")}
246286
</Button>
247287
<Button
248288
variant="danger"
249-
className="flex-grow-1 flex-md-grow-0 d-flex align-items-center justify-content-center gap-2"
289+
size="sm"
290+
className="d-flex align-items-center gap-2"
250291
onClick={() => handleDeleteToken(token.id)}
251292
>
252-
<Trash2 />
293+
<Trash2 size={16} />
253294
{t("tokens.delete")}
254295
</Button>
255296
</div>
256-
</InputGroup>
257-
{index !== tokens.length - 1 ? <hr class="d-block d-md-none"/> : <></>}
258-
</>
297+
{index !== tokens.length - 1 && <hr className="mt-3" />}
298+
</div>
259299
))}
260300
</Card.Body>
261301
</Card>

frontend/src/schema.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,7 @@ export interface components {
531531
user_id?: string | null;
532532
};
533533
CreateAuthorizationTokenSchema: {
534+
name: string;
534535
use_once: boolean;
535536
};
536537
DeleteAuthorizationTokenSchema: {
@@ -643,7 +644,12 @@ export interface components {
643644
secret_salt: number[];
644645
};
645646
ResponseAuthorizationToken: {
647+
/** Format: int64 */
648+
created_at: number;
646649
id: string;
650+
/** Format: int64 */
651+
last_used_at?: number | null;
652+
name: string;
647653
token: string;
648654
use_once: boolean;
649655
};

0 commit comments

Comments
 (0)