11import { useEffect , useState } from 'preact/hooks' ;
22import { 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' ;
44import { showAlert } from '../components/Alert' ;
55import { Base64 } from 'js-base64' ;
66import { encodeBase58Flickr } from '../base58' ;
77import { useTranslation } from 'react-i18next' ;
88import { Clipboard , Trash2 } from 'react-feather' ;
99import { components } from '../schema' ;
1010import { ArgonType , hash } from 'argon2-browser' ;
11+ import sodium from 'libsodium-wrappers' ;
1112
1213async 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 >
0 commit comments