@@ -169,7 +169,13 @@ struct WsListResponse { workspaces: Vec<WsItem> }
169169struct WsItem { public_id : String , name : String }
170170
171171/// Wait for the browser callback, verify state, and extract the authorization code.
172- fn receive_callback ( server : & tiny_http:: Server , expected_state : & str ) -> Result < String , String > {
172+ /// `success_title` and `success_body` are rendered in the browser tab on success.
173+ fn receive_callback (
174+ server : & tiny_http:: Server ,
175+ expected_state : & str ,
176+ success_title : & str ,
177+ success_body : & str ,
178+ ) -> Result < String , String > {
173179 let request = server. recv ( ) . map_err ( |e| format ! ( "failed to receive callback: {e}" ) ) ?;
174180 let raw_url = request. url ( ) . to_string ( ) ;
175181 let params = parse_query_params ( & raw_url) ;
@@ -187,33 +193,34 @@ fn receive_callback(server: &tiny_http::Server, expected_state: &str) -> Result<
187193 }
188194 } ;
189195
190- let html = r#"<!DOCTYPE html>
196+ let html = format ! (
197+ r#"<!DOCTYPE html>
191198<html lang="en">
192199<head>
193200 <meta charset="UTF-8" />
194201 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
195- <title>Hotdata — Login Successful </title>
202+ <title>Hotdata — {success_title} </title>
196203 <style>
197- *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
198- body {
204+ *, *::before, *::after {{ box-sizing: border-box; margin: 0; padding: 0; } }
205+ body {{
199206 font-family: ui-sans-serif, system-ui, -apple-system, sans-serif;
200207 background: #111827;
201208 color: #e5e7eb;
202209 display: flex;
203210 align-items: center;
204211 justify-content: center;
205212 min-height: 100vh;
206- }
207- .card {
213+ }}
214+ .card {{
208215 background: #1f2937;
209216 border: 1px solid #374151;
210217 border-radius: 0.5rem;
211218 padding: 2.5rem;
212219 max-width: 420px;
213220 width: 100%;
214221 text-align: center;
215- }
216- .icon {
222+ }}
223+ .icon {{
217224 width: 48px;
218225 height: 48px;
219226 background: #14532d;
@@ -222,10 +229,10 @@ fn receive_callback(server: &tiny_http::Server, expected_state: &str) -> Result<
222229 align-items: center;
223230 justify-content: center;
224231 margin: 0 auto 1.25rem;
225- }
226- .icon svg { width: 24px; height: 24px; stroke: #86efac; }
227- h1 { font-size: 1.25rem; font-weight: 600; color: #f3f4f6; margin-bottom: 0.5rem; }
228- p { font-size: 0.875rem; color: #9ca3af; line-height: 1.5; }
232+ }}
233+ .icon svg {{ width: 24px; height: 24px; stroke: #86efac; } }
234+ h1 {{ font-size: 1.25rem; font-weight: 600; color: #f3f4f6; margin-bottom: 0.5rem; } }
235+ p {{ font-size: 0.875rem; color: #9ca3af; line-height: 1.5; } }
229236 </style>
230237</head>
231238<body>
@@ -235,11 +242,12 @@ fn receive_callback(server: &tiny_http::Server, expected_state: &str) -> Result<
235242 <path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" />
236243 </svg>
237244 </div>
238- <h1>Login successful </h1>
239- <p>You're now authenticated with Hotdata.<br/>You can close this tab and return to the terminal. </p>
245+ <h1>{success_title} </h1>
246+ <p>{success_body} </p>
240247 </div>
241248</body>
242- </html>"# ;
249+ </html>"#
250+ ) ;
243251 let response = tiny_http:: Response :: from_string ( html) . with_header (
244252 "Content-Type: text/html"
245253 . parse :: < tiny_http:: Header > ( )
@@ -318,7 +326,12 @@ pub fn login() {
318326
319327 println ! ( "Waiting for login callback..." ) ;
320328
321- let code = match receive_callback ( & server, & state) {
329+ let code = match receive_callback (
330+ & server,
331+ & state,
332+ "Login successful" ,
333+ "You're now authenticated with Hotdata.<br/>You can close this tab and return to the terminal." ,
334+ ) {
322335 Ok ( c) => c,
323336 Err ( e) => {
324337 eprintln ! ( "error: {e}" ) ;
@@ -358,6 +371,107 @@ pub fn login() {
358371 }
359372}
360373
374+ pub fn register ( ) {
375+ let profile_config = config:: load ( "default" ) . unwrap_or_default ( ) ;
376+ let app_url = profile_config. app_url . to_string ( ) ;
377+
378+ if is_already_signed_in ( & profile_config) {
379+ println ! (
380+ "{}" ,
381+ "You are already signed in. Use 'hotdata auth login' to log in with a different account." . green( )
382+ ) ;
383+ return ;
384+ }
385+
386+ let code_verifier = generate_code_verifier ( ) ;
387+ let code_challenge = generate_code_challenge ( & code_verifier) ;
388+ let state = generate_random_string ( 32 ) ;
389+
390+ let server =
391+ tiny_http:: Server :: http ( "127.0.0.1:0" ) . expect ( "failed to start local callback server" ) ;
392+ let port = server. server_addr ( ) . to_ip ( ) . unwrap ( ) . port ( ) ;
393+
394+ let register_url = format ! (
395+ "{app_url}/auth/cli-register/\
396+ ?code_challenge={code_challenge}\
397+ &code_challenge_method=S256\
398+ &state={state}\
399+ &callback_port={port}",
400+ app_url = app_url. trim_end_matches( '/' ) ,
401+ ) ;
402+
403+ println ! ( "Opening browser to create your account..." ) ;
404+ stdout ( )
405+ . execute ( Print ( "If your browser does not open, visit:\n " ) )
406+ . unwrap ( )
407+ . execute ( SetForegroundColor ( Color :: DarkGrey ) )
408+ . unwrap ( )
409+ . execute ( Print ( format ! ( "{register_url}\n " ) ) )
410+ . unwrap ( )
411+ . execute ( ResetColor )
412+ . unwrap ( ) ;
413+
414+ if let Err ( e) = open:: that ( & register_url) {
415+ eprintln ! ( "failed to open browser: {e}" ) ;
416+ }
417+
418+ println ! ( "Waiting for account setup to complete..." ) ;
419+
420+ let code = match receive_callback (
421+ & server,
422+ & state,
423+ "Account created" ,
424+ "Your Hotdata account is ready.<br/>You can close this tab and return to the terminal." ,
425+ ) {
426+ Ok ( c) => c,
427+ Err ( e) => {
428+ eprintln ! ( "error: {e}" ) ;
429+ std:: process:: exit ( 1 ) ;
430+ }
431+ } ;
432+
433+ match crate :: jwt:: exchange_cli_register_code ( & profile_config, & code, & code_verifier) {
434+ Ok ( session) => {
435+ if let Err ( e) = crate :: jwt:: save_session ( & session) {
436+ eprintln ! ( "warning: could not save session: {e}" ) ;
437+ }
438+ stdout ( )
439+ . execute ( SetForegroundColor ( Color :: Green ) )
440+ . unwrap ( )
441+ . execute ( Print ( "Account created and logged in.\n " ) )
442+ . unwrap ( )
443+ . execute ( ResetColor )
444+ . unwrap ( ) ;
445+
446+ let workspaces = cache_workspaces ( & profile_config, & session. access_token )
447+ . unwrap_or ( profile_config. workspaces ) ;
448+ match workspaces. first ( ) {
449+ Some ( w) => {
450+ print_row (
451+ "Workspace" ,
452+ & format ! (
453+ "{} {}" ,
454+ w. name. as_str( ) . cyan( ) ,
455+ format!( "({})" , w. public_id) . dark_grey( )
456+ ) ,
457+ ) ;
458+ print_row (
459+ "" ,
460+ & "use 'hotdata workspaces set' to switch workspaces"
461+ . dark_grey ( )
462+ . to_string ( ) ,
463+ ) ;
464+ }
465+ None => print_row ( "Workspace" , & "None" . dark_grey ( ) . to_string ( ) ) ,
466+ }
467+ }
468+ Err ( msg) => {
469+ eprintln ! ( "{}" , msg. red( ) ) ;
470+ std:: process:: exit ( 1 ) ;
471+ }
472+ }
473+ }
474+
361475/// Fetch workspaces with a freshly minted JWT and cache them in config.
362476/// Returns the freshly fetched list so callers can display it without
363477/// having to reload config from disk.
@@ -650,7 +764,7 @@ mod tests {
650764 . unwrap ( ) ;
651765 } ) ;
652766
653- let result = receive_callback ( & server, "expected-state" ) ;
767+ let result = receive_callback ( & server, "expected-state" , "" , "" ) ;
654768 handle. join ( ) . unwrap ( ) ;
655769
656770 assert_eq ! ( result. unwrap( ) , "test-auth-code" ) ;
@@ -670,7 +784,7 @@ mod tests {
670784 . send ( ) ;
671785 } ) ;
672786
673- let result = receive_callback ( & server, "expected-state" ) ;
787+ let result = receive_callback ( & server, "expected-state" , "" , "" ) ;
674788 handle. join ( ) . unwrap ( ) ;
675789
676790 assert ! ( result. is_err( ) ) ;
@@ -754,7 +868,7 @@ mod tests {
754868 . send ( ) ;
755869 } ) ;
756870
757- let result = receive_callback ( & server, "expected-state" ) ;
871+ let result = receive_callback ( & server, "expected-state" , "" , "" ) ;
758872 handle. join ( ) . unwrap ( ) ;
759873
760874 assert ! ( result. is_err( ) ) ;
0 commit comments