@@ -280,3 +280,137 @@ pub mod benchmark {
280280 Ok ( ( ) )
281281 }
282282}
283+
284+ pub mod auth {
285+ use anyhow:: { anyhow, Context , Result } ;
286+ use open;
287+ use reqwest:: StatusCode ;
288+ use serde:: { Deserialize , Serialize } ;
289+ use std:: time:: Duration ;
290+ use tokio:: time:: sleep;
291+
292+ /// Default UI/API base URL. Change this constant to point to your deployment.
293+ pub const AUTH_BASE_URL : & str = "http://localhost:3000" ;
294+
295+ fn get_api_base_url ( ) -> String {
296+ // Allow override via ENVIO_API_URL, otherwise use the constant above.
297+ std:: env:: var ( "ENVIO_API_URL" ) . unwrap_or_else ( |_| AUTH_BASE_URL . to_string ( ) )
298+ }
299+
300+ #[ derive( Debug , Deserialize ) ]
301+ #[ serde( rename_all = "camelCase" ) ]
302+ struct CliAuthSession {
303+ code : String ,
304+ auth_url : String ,
305+ expires_in : i32 ,
306+ }
307+
308+ #[ derive( Debug , Deserialize ) ]
309+ struct User {
310+ #[ allow( dead_code) ]
311+ id : Option < String > ,
312+ #[ allow( dead_code) ]
313+ name : Option < String > ,
314+ #[ allow( dead_code) ]
315+ email : Option < String > ,
316+ #[ allow( dead_code) ]
317+ #[ serde( rename = "githubId" ) ]
318+ github_id : Option < String > ,
319+ #[ allow( dead_code) ]
320+ #[ serde( rename = "githubLogin" ) ]
321+ github_login : Option < String > ,
322+ }
323+
324+ #[ derive( Debug , Deserialize ) ]
325+ struct CliAuthStatus {
326+ completed : bool ,
327+ #[ allow( dead_code) ]
328+ user : Option < User > ,
329+ #[ allow( dead_code) ]
330+ error : Option < String > ,
331+ token : Option < String > ,
332+ }
333+
334+ #[ derive( Debug , Serialize ) ]
335+ struct EmptyBody { }
336+
337+ pub async fn run_auth ( ) -> Result < ( ) > {
338+ let base = get_api_base_url ( ) ;
339+ let client = reqwest:: Client :: new ( ) ;
340+
341+ // 1) Create a CLI auth session
342+ let create_url = format ! ( "{}/api/auth/cli-session" , base) ;
343+ let session: CliAuthSession = client
344+ . post ( & create_url)
345+ . json ( & EmptyBody { } )
346+ . send ( )
347+ . await
348+ . with_context ( || format ! ( "Failed to POST {}" , create_url) ) ?
349+ . error_for_status ( )
350+ . with_context ( || format ! ( "Non-200 from {}" , create_url) ) ?
351+ . json ( )
352+ . await
353+ . context ( "Failed to decode CLI session response" ) ?;
354+
355+ println ! ( "Opening browser for authentication...\n If it doesn't open, visit: {}" , session. auth_url) ;
356+ let _ = open:: that_detached ( & session. auth_url ) ;
357+
358+ // 2) Poll for completion
359+ let poll_url = format ! ( "{}/api/auth/cli-session?code={}" , base, session. code) ;
360+ let poll_interval = Duration :: from_secs ( 2 ) ;
361+ // Add a small grace window to handle cold starts or UI recompiles wiping in-memory state
362+ let extra_grace_attempts = 15 ; // ~30s grace
363+ let max_attempts = ( session. expires_in . max ( 0 ) as u64 ) / 2 + extra_grace_attempts;
364+
365+ // Give the UI a brief warm-up before first poll
366+ sleep ( Duration :: from_secs ( 2 ) ) . await ;
367+
368+ let mut consecutive_not_found = 0u32 ;
369+
370+ for _ in 0 ..max_attempts {
371+ sleep ( poll_interval) . await ;
372+
373+ let resp = match client. get ( & poll_url) . send ( ) . await {
374+ Ok ( r) => r,
375+ Err ( _) => {
376+ // transient network error; try again
377+ continue ;
378+ }
379+ } ;
380+
381+ if resp. status ( ) == StatusCode :: NOT_FOUND {
382+ consecutive_not_found += 1 ;
383+ // Keep polling; in-memory session store may not be ready yet
384+ if consecutive_not_found % 10 == 1 {
385+ // Print a lightweight status occasionally
386+ eprintln ! ( "Waiting for session to become available..." ) ;
387+ }
388+ continue ;
389+ } else {
390+ consecutive_not_found = 0 ;
391+ }
392+
393+ if resp. status ( ) . is_success ( ) {
394+ let status: CliAuthStatus = match resp. json ( ) . await {
395+ Ok ( s) => s,
396+ Err ( _) => continue ,
397+ } ;
398+
399+ if let Some ( err) = status. error {
400+ if !err. is_empty ( ) {
401+ return Err ( anyhow ! ( "authentication error: {}" , err) ) ;
402+ }
403+ }
404+
405+ if status. completed {
406+ if let Some ( token) = status. token {
407+ println ! ( "{}" , token) ;
408+ return Ok ( ( ) ) ;
409+ }
410+ }
411+ }
412+ }
413+
414+ Err ( anyhow ! ( "authentication timed out" ) )
415+ }
416+ }
0 commit comments