@@ -2,25 +2,100 @@ import { createFileRoute } from '@tanstack/react-router'
22import { Card , CardContent , CardDescription , CardHeader , CardTitle } from '@/components/ui/card'
33import { Button } from '@/components/ui/button'
44import { Input } from '@/components/ui/input'
5+ import { Label } from '@/components/ui/label'
6+ import { Dialog , DialogContent , DialogDescription , DialogFooter , DialogHeader , DialogTitle , DialogTrigger } from '@/components/ui/dialog'
7+ import { Select , SelectContent , SelectItem , SelectTrigger , SelectValue } from '@/components/ui/select'
58import { useState } from 'react'
9+ import { createTokenFn , deleteTokenFn , getTokensFn } from '@/api/tokens_serverFunctions'
10+ import { useToast } from '@/hooks/use-toast'
11+ import { Table , TableBody , TableCell , TableHead , TableHeader , TableRow } from '@/components/ui/table'
612
713export const Route = createFileRoute (
814 '/_authenticated/_dashboard/dashboard/settings/tokens' ,
915) ( {
1016 component : RouteComponent ,
17+ loader : async ( { context } ) => {
18+ const { user, organisationId } = context ;
19+ const tokens = await getTokensFn ( { data : { organizationId : organisationId , userId : user ?. id || '' } } )
20+ return { tokens, user, organisationId }
21+ }
1122} )
1223
1324function RouteComponent ( ) {
14- const [ tokens , setTokens ] = useState < string [ ] > ( [ ] )
25+ const { tokens, user, organisationId } = Route . useLoaderData ( )
26+ const [ tokenList , setTokenList ] = useState < typeof tokens > ( tokens )
1527 const [ newToken , setNewToken ] = useState ( '' )
28+ const [ open , setOpen ] = useState ( false )
29+ const [ nickname , setNickname ] = useState ( '' )
30+ const [ expiry , setExpiry ] = useState < '1_week' | '30_days' | 'no_expiry' > ( '1_week' )
31+ const [ submitting , setSubmitting ] = useState ( false )
32+ const { toast } = useToast ( )
33+ const computeExpiry = ( value : '1_week' | '30_days' | 'no_expiry' ) : string | null => {
34+ console . log ( 'value' , value )
35+ if ( value === 'no_expiry' ) return null
36+ if ( value === '1_week' ) return `${ 7 * 24 } h`
37+ if ( value === '30_days' ) return `${ 30 * 24 } h`
38+ return `${ 7 * 24 } h`
39+ }
40+
41+ function formatDateString ( value ?: string | null ) {
42+ if ( ! value ) return '—'
43+ const d = new Date ( value )
44+ if ( isNaN ( d . getTime ( ) ) ) return String ( value )
45+ return d . toLocaleDateString ( 'en-US' , {
46+ year : 'numeric' ,
47+ month : 'short' ,
48+ day : 'numeric' ,
49+ hour : '2-digit' ,
50+ minute : '2-digit'
51+ } )
52+ }
53+
54+ function isTokenExpired ( token : any ) {
55+ if ( token ?. status && token . status !== 'active' ) return true
56+ if ( token ?. expires_at ) {
57+ const exp = new Date ( token . expires_at )
58+ if ( ! isNaN ( exp . getTime ( ) ) && exp . getTime ( ) < Date . now ( ) ) return true
59+ }
60+ return false
61+ }
1662
17- const generateToken = ( ) => {
18- // This is a placeholder - implement actual token generation logic
19- const token = `digger_${ Math . random ( ) . toString ( 36 ) . substring ( 2 ) } `
20- setTokens ( [ ...tokens , token ] )
21- setNewToken ( token )
63+ const onConfirmGenerate = async ( ) => {
64+ setSubmitting ( true )
65+ try {
66+ const expiresAt = computeExpiry ( expiry )
67+ const created = await createTokenFn ( { data : { organizationId : organisationId , userId : user ?. id || '' , name : nickname || 'New Token' , expiresAt} } )
68+ if ( created && created . token ) {
69+ setNewToken ( created . token )
70+ }
71+ setOpen ( false )
72+ setNickname ( '' )
73+ setExpiry ( 'no_expiry' )
74+ const newTokenList = await getTokensFn ( { data : { organizationId : organisationId , userId : user ?. id || '' } } )
75+ setTokenList ( newTokenList )
76+ } finally {
77+ setSubmitting ( false )
78+ }
2279 }
2380
81+ const handleRevokeToken = async ( tokenId : string ) => {
82+ deleteTokenFn ( { data : { organizationId : organisationId , userId : user ?. id || '' , tokenId : tokenId } } ) . then ( ( ) => {
83+ toast ( {
84+ title : 'Token revoked' ,
85+ description : 'The token has been revoked' ,
86+ } )
87+ } ) . catch ( ( error ) => {
88+ toast ( {
89+ title : 'Failed to revoke token' ,
90+ description : error . message ,
91+ variant : 'destructive' ,
92+ } )
93+ } ) . finally ( async ( ) => {
94+ setSubmitting ( false )
95+ const newTokenList = await getTokensFn ( { data : { organizationId : organisationId , userId : user ?. id || '' } } )
96+ setTokenList ( newTokenList )
97+ } )
98+ }
2499 return (
25100 < Card >
26101 < CardHeader >
@@ -31,7 +106,40 @@ function RouteComponent() {
31106 </ CardHeader >
32107 < CardContent className = "space-y-4" >
33108 < div className = "flex space-x-4" >
34- < Button onClick = { generateToken } > Generate New Token</ Button >
109+ < Dialog open = { open } onOpenChange = { setOpen } >
110+ < DialogTrigger asChild >
111+ < Button > Generate New Token</ Button >
112+ </ DialogTrigger >
113+ < DialogContent >
114+ < DialogHeader >
115+ < DialogTitle > Generate API Token</ DialogTitle >
116+ < DialogDescription > Provide a nickname and choose an expiry.</ DialogDescription >
117+ </ DialogHeader >
118+ < div className = "space-y-4" >
119+ < div className = "space-y-2" >
120+ < Label htmlFor = "nickname" > Nickname</ Label >
121+ < Input id = "nickname" placeholder = "e.g. CI token" value = { nickname } onChange = { ( e ) => setNickname ( e . target . value ) } />
122+ </ div >
123+ < div className = "space-y-2" >
124+ < Label > Expiry</ Label >
125+ < Select value = { expiry } onValueChange = { ( v ) => setExpiry ( v as typeof expiry ) } >
126+ < SelectTrigger >
127+ < SelectValue placeholder = "Select expiry" />
128+ </ SelectTrigger >
129+ < SelectContent >
130+ < SelectItem value = "1_week" > 1 week</ SelectItem >
131+ < SelectItem value = "30_days" > 30 days</ SelectItem >
132+ < SelectItem value = "no_expiry" > No expiry</ SelectItem >
133+ </ SelectContent >
134+ </ Select >
135+ </ div >
136+ </ div >
137+ < DialogFooter >
138+ < Button variant = "outline" onClick = { ( ) => setOpen ( false ) } disabled = { submitting } > Cancel</ Button >
139+ < Button onClick = { onConfirmGenerate } disabled = { submitting || ( ! nickname && expiry === 'no_expiry' ) } > { submitting ? 'Generating...' : 'Generate' } </ Button >
140+ </ DialogFooter >
141+ </ DialogContent >
142+ </ Dialog >
35143 </ div >
36144 { newToken && (
37145 < div className = "space-y-2" >
@@ -46,23 +154,43 @@ function RouteComponent() {
46154 ) }
47155 < div className = "space-y-2" >
48156 < h4 className = "text-sm font-medium" > Your Tokens</ h4 >
49- { tokens . length === 0 ? (
157+ { tokenList . length === 0 ? (
50158 < p className = "text-sm text-muted-foreground" > No tokens generated yet</ p >
51159 ) : (
52- < div className = "space-y-2" >
53- { tokens . map ( ( token , index ) => (
54- < div key = { index } className = "flex items-center justify-between" >
55- < code className = "text-sm" > •••••••••••{ token . slice ( - 4 ) } </ code >
56- < Button
57- variant = "destructive"
58- size = "sm"
59- onClick = { ( ) => setTokens ( tokens . filter ( ( _ , i ) => i !== index ) ) }
60- >
61- Revoke
62- </ Button >
63- </ div >
64- ) ) }
65- </ div >
160+ < Table >
161+ < TableHeader >
162+ < TableRow >
163+ < TableHead className = "text-left" > Name</ TableHead >
164+ < TableHead className = "text-left" > Token</ TableHead >
165+ < TableHead className = "text-left" > Expires</ TableHead >
166+ < TableHead className = "text-left" > Created</ TableHead >
167+ < TableHead className = "text-left" > Actions</ TableHead >
168+ </ TableRow >
169+ </ TableHeader >
170+ < TableBody >
171+ { tokenList . map ( ( token , index ) => (
172+ < TableRow key = { index } >
173+ < TableCell className = "font-medium" > { token . name } </ TableCell >
174+ < TableCell > •••••••••••{ token . token . slice ( - 4 ) } </ TableCell >
175+ < TableCell >
176+ { isTokenExpired ( token )
177+ ? < span className = "text-destructive" > This token has expired</ span >
178+ : ( token . expires_at ? formatDateString ( token . expires_at ) : 'No expiry' ) }
179+ </ TableCell >
180+ < TableCell > { formatDateString ( token . created_at ) } </ TableCell >
181+ < TableCell >
182+ < Button
183+ variant = "destructive"
184+ size = "sm"
185+ onClick = { ( ) => handleRevokeToken ( token . id ) }
186+ >
187+ Revoke
188+ </ Button >
189+ </ TableCell >
190+ </ TableRow >
191+ ) ) }
192+ </ TableBody >
193+ </ Table >
66194 ) }
67195 </ div >
68196 </ CardContent >
0 commit comments