1+ // src/pages/admin/components/CreateTokenModal.tsx
2+ import { useState } from "react" ;
3+ import { apiBaseUrl } from "@/api/api" ;
4+
5+ type CreateTokenModalProps = {
6+ isOpen : boolean ;
7+ onClose : ( ) => void ;
8+ onTokenCreated : ( ) => void ;
9+ } ;
10+
11+ type CreateState =
12+ | { status : "form" }
13+ | { status : "creating" }
14+ | { status : "success" ; token : string ; tokenId : string }
15+ | { status : "error" ; message : string } ;
16+
17+ export function CreateTokenModal ( { isOpen, onClose, onTokenCreated } : CreateTokenModalProps ) {
18+ const [ label , setLabel ] = useState ( "" ) ;
19+ const [ state , setState ] = useState < CreateState > ( { status : "form" } ) ;
20+ const [ copied , setCopied ] = useState ( false ) ;
21+
22+ if ( ! isOpen ) return null ;
23+
24+ const handleCreate = async ( ) => {
25+ if ( ! label . trim ( ) ) {
26+ setState ( { status : "error" , message : "Label is required" } ) ;
27+ return ;
28+ }
29+
30+ setState ( { status : "creating" } ) ;
31+
32+ try {
33+ const res = await fetch ( `${ apiBaseUrl } /admin/tokens` , {
34+ method : "POST" ,
35+ credentials : "include" ,
36+ headers : {
37+ "Content-Type" : "application/json" ,
38+ Accept : "application/json" ,
39+ } ,
40+ body : JSON . stringify ( {
41+ label : label . trim ( ) ,
42+ scopes : [ "admin" ] ,
43+ expires_at : null , // Never expires
44+ } ) ,
45+ } ) ;
46+
47+ if ( ! res . ok ) {
48+ const errorText = await res . text ( ) ;
49+ throw new Error ( `Failed to create token: ${ res . status } ${ errorText } ` ) ;
50+ }
51+
52+ const json = await res . json ( ) ;
53+
54+ if ( ! json . ok || ! json . data ?. token ) {
55+ throw new Error ( "Invalid response from server" ) ;
56+ }
57+
58+ setState ( {
59+ status : "success" ,
60+ token : json . data . token ,
61+ tokenId : json . data . id ,
62+ } ) ;
63+ } catch ( e : any ) {
64+ setState ( {
65+ status : "error" ,
66+ message : e ?. message ?? "Failed to create token" ,
67+ } ) ;
68+ }
69+ } ;
70+
71+ const handleCopy = async ( ) => {
72+ if ( state . status === "success" ) {
73+ await navigator . clipboard . writeText ( state . token ) ;
74+ setCopied ( true ) ;
75+ setTimeout ( ( ) => setCopied ( false ) , 2000 ) ;
76+ }
77+ } ;
78+
79+ const handleDone = ( ) => {
80+ onTokenCreated ( ) ;
81+ onClose ( ) ;
82+ // Reset state for next time
83+ setState ( { status : "form" } ) ;
84+ setLabel ( "" ) ;
85+ setCopied ( false ) ;
86+ } ;
87+
88+ return (
89+ < div className = "fixed inset-0 z-50 flex items-center justify-center bg-black/80" >
90+ < div className = "bg-zinc-900 border border-zinc-700 rounded-lg p-6 max-w-lg w-full mx-4 shadow-2xl" >
91+ { /* Form State */ }
92+ { state . status === "form" && (
93+ < >
94+ < h3 className = "text-xl font-semibold text-zinc-100 mb-4" >
95+ Create API Token
96+ </ h3 >
97+ < div className = "mb-4" >
98+ < label className = "block text-sm text-zinc-400 mb-2" >
99+ Token Label
100+ </ label >
101+ < input
102+ type = "text"
103+ value = { label }
104+ onChange = { ( e ) => setLabel ( e . target . value ) }
105+ placeholder = "e.g., Claude AI Agent"
106+ className = "w-full px-3 py-2 bg-zinc-800 border border-zinc-700 rounded text-zinc-100 placeholder-zinc-500 focus:outline-none focus:border-teal-500"
107+ autoFocus
108+ />
109+ </ div >
110+ < div className = "text-xs text-zinc-500 mb-4" >
111+ This token will have < code className = "text-teal-400" > admin</ code > scope
112+ and never expire.
113+ </ div >
114+ < div className = "flex gap-3 justify-end" >
115+ < button
116+ onClick = { onClose }
117+ className = "px-4 py-2 text-zinc-400 hover:text-zinc-200 transition-colors"
118+ >
119+ Cancel
120+ </ button >
121+ < button
122+ onClick = { handleCreate }
123+ className = "px-4 py-2 bg-teal-600 hover:bg-teal-500 text-white rounded transition-colors"
124+ >
125+ Generate Token
126+ </ button >
127+ </ div >
128+ </ >
129+ ) }
130+
131+ { /* Creating State */ }
132+ { state . status === "creating" && (
133+ < div className = "text-center py-8" >
134+ < div className = "text-zinc-300 mb-2" > Generating token...</ div >
135+ < div className = "text-zinc-500 text-sm" > This will only take a moment</ div >
136+ </ div >
137+ ) }
138+
139+ { /* Success State */ }
140+ { state . status === "success" && (
141+ < >
142+ < h3 className = "text-xl font-semibold text-teal-400 mb-4" >
143+ ✅ Token Created!
144+ </ h3 >
145+ < div className = "bg-red-900/20 border border-red-700 rounded p-4 mb-4" >
146+ < div className = "text-red-400 font-semibold mb-2" >
147+ ⚠️ Copy this token now!
148+ </ div >
149+ < div className = "text-red-300 text-sm" >
150+ For security reasons, this token will only be shown once.
151+ If you lose it, you'll need to create a new one.
152+ </ div >
153+ </ div >
154+
155+ < div className = "mb-4" >
156+ < label className = "block text-sm text-zinc-400 mb-2" >
157+ Your Bearer Token
158+ </ label >
159+ < div className = "bg-zinc-800 border border-zinc-700 rounded p-3 font-mono text-xs text-teal-300 break-all" >
160+ { state . token }
161+ </ div >
162+ </ div >
163+
164+ < div className = "bg-zinc-800 border border-zinc-700 rounded p-3 mb-4 text-xs text-zinc-400" >
165+ < div className = "font-semibold text-zinc-300 mb-2" >
166+ 💡 How to use this token:
167+ </ div >
168+ < div className = "space-y-1" >
169+ < div > 1. Copy the token above</ div >
170+ < div > 2. Give it to your AI agent (Claude, GPT, etc.)</ div >
171+ < div > 3. Agent can use it to create Lab Notes autonomously</ div >
172+ </ div >
173+ </ div >
174+
175+ < div className = "flex gap-3 justify-end" >
176+ < button
177+ onClick = { handleCopy }
178+ className = "px-4 py-2 bg-zinc-700 hover:bg-zinc-600 text-zinc-200 rounded transition-colors"
179+ >
180+ { copied ? "✓ Copied!" : "Copy Token" }
181+ </ button >
182+ < button
183+ onClick = { handleDone }
184+ className = "px-4 py-2 bg-teal-600 hover:bg-teal-500 text-white rounded transition-colors"
185+ >
186+ Done
187+ </ button >
188+ </ div >
189+ </ >
190+ ) }
191+
192+ { /* Error State */ }
193+ { state . status === "error" && (
194+ < >
195+ < h3 className = "text-xl font-semibold text-red-400 mb-4" > Error</ h3 >
196+ < div className = "bg-red-900/20 border border-red-700 rounded p-4 mb-4" >
197+ < div className = "text-red-300" > { state . message } </ div >
198+ </ div >
199+ < div className = "flex gap-3 justify-end" >
200+ < button
201+ onClick = { ( ) => setState ( { status : "form" } ) }
202+ className = "px-4 py-2 bg-zinc-700 hover:bg-zinc-600 text-zinc-200 rounded transition-colors"
203+ >
204+ Try Again
205+ </ button >
206+ </ div >
207+ </ >
208+ ) }
209+ </ div >
210+ </ div >
211+ ) ;
212+ }
0 commit comments