Skip to content

Commit 07dec12

Browse files
committed
Refine auth code fallback prompt
1 parent 9ded084 commit 07dec12

1 file changed

Lines changed: 43 additions & 149 deletions

File tree

src/auth.rs

Lines changed: 43 additions & 149 deletions
Original file line numberDiff line numberDiff line change
@@ -10,23 +10,14 @@ use std::collections::HashMap;
1010
use std::fs;
1111
use std::io::{self, BufRead, BufReader, IsTerminal, Write};
1212
use std::net::{TcpListener, TcpStream};
13-
use std::path::Path;
14-
use std::process::{Command, Stdio};
1513
use std::thread;
16-
use tokio::{signal, sync::mpsc};
17-
18-
#[cfg(target_os = "macos")]
19-
const CLIPBOARD_COMMANDS: &[(&str, &[&str])] = &[("pbcopy", &[])];
20-
21-
#[cfg(target_os = "windows")]
22-
const CLIPBOARD_COMMANDS: &[(&str, &[&str])] = &[("cmd", &["/C", "clip"])];
14+
use tokio::{
15+
signal,
16+
sync::mpsc,
17+
time::{sleep, Duration},
18+
};
2319

24-
#[cfg(all(unix, not(target_os = "macos")))]
25-
const CLIPBOARD_COMMANDS: &[(&str, &[&str])] = &[
26-
("wl-copy", &[]),
27-
("xclip", &["-selection", "clipboard"]),
28-
("xsel", &["--clipboard", "--input"]),
29-
];
20+
const AUTH_CODE_PROMPT_DELAY: Duration = Duration::from_secs(3);
3021

3122
#[derive(Serialize, Deserialize)]
3223
struct Credentials {
@@ -188,74 +179,13 @@ fn pkce_challenge_from_verifier(verifier: &str) -> String {
188179
URL_SAFE_NO_PAD.encode(hash.as_ref())
189180
}
190181

191-
fn command_exists(program: &str) -> bool {
192-
let path = Path::new(program);
193-
if path.is_absolute() {
194-
return path.is_file();
195-
}
196-
197-
std::env::var_os("PATH")
198-
.map(|paths| std::env::split_paths(&paths).any(|dir| dir.join(program).is_file()))
199-
.unwrap_or(false)
200-
}
201-
202-
fn can_copy_to_clipboard() -> bool {
203-
#[cfg(target_os = "windows")]
204-
{
205-
true
206-
}
207-
#[cfg(not(target_os = "windows"))]
208-
{
209-
CLIPBOARD_COMMANDS
210-
.iter()
211-
.any(|(program, _)| command_exists(program))
212-
}
213-
}
214-
215-
fn copy_to_clipboard(text: &str) -> Result<()> {
216-
let mut last_error = None;
217-
for (program, args) in CLIPBOARD_COMMANDS {
218-
let mut child = match Command::new(program)
219-
.args(*args)
220-
.stdin(Stdio::piped())
221-
.spawn()
222-
{
223-
Ok(child) => child,
224-
Err(error) => {
225-
last_error = Some(error);
226-
continue;
227-
}
228-
};
229-
230-
let mut stdin = child
231-
.stdin
232-
.take()
233-
.ok_or_else(|| anyhow::anyhow!("Failed to open clipboard command stdin"))?;
234-
stdin.write_all(text.as_bytes())?;
235-
drop(stdin);
236-
237-
if child.wait()?.success() {
238-
return Ok(());
239-
}
240-
}
241-
242-
match last_error {
243-
Some(error) => Err(error.into()),
244-
None => bail!("No clipboard command available"),
245-
}
246-
}
247-
248182
fn print_opening_browser() {
249183
eprintln!("Opening browser to sign in…");
250184
}
251185

252-
fn print_auth_url(auth_url: &str, can_copy_url: bool) {
186+
fn print_auth_url(auth_url: &str) {
253187
eprintln!();
254-
if can_copy_url {
255-
eprintln!("Browser didn't open? Use the url below to sign in (c to copy)");
256-
} else {
257-
eprintln!("Browser didn't open? Use the url below to sign in");
258-
}
188+
eprintln!("Browser didn't open? Use the url below to sign in");
259189
eprintln!();
260190
eprintln!("{auth_url}");
261191
eprintln!();
@@ -379,15 +309,9 @@ fn start_callback_server(
379309
Ok(callback_url)
380310
}
381311

382-
fn read_auth_code(
383-
stdin_is_terminal: bool,
384-
auth_url: &str,
385-
can_copy_url: bool,
386-
) -> Result<Option<String>> {
387-
if stdin_is_terminal {
388-
eprint!("Paste code here if prompted > ");
389-
let _ = io::stderr().flush();
390-
}
312+
fn read_auth_code() -> Result<Option<String>> {
313+
eprint!("Paste code here if prompted > ");
314+
let _ = io::stderr().flush();
391315

392316
let mut input = String::new();
393317
let bytes_read = io::stdin().lock().read_line(&mut input)?;
@@ -397,30 +321,15 @@ fn read_auth_code(
397321

398322
let code = input.trim().to_string();
399323
if code.is_empty() {
400-
if stdin_is_terminal {
401-
return Ok(None);
402-
}
403-
bail!("Authentication cancelled");
404-
}
405-
if stdin_is_terminal && can_copy_url && code.eq_ignore_ascii_case("c") {
406-
match copy_to_clipboard(auth_url) {
407-
Ok(()) => eprintln!("Copied sign-in URL to clipboard."),
408-
Err(error) => eprintln!("Couldn't copy sign-in URL: {error}"),
409-
}
410324
return Ok(None);
411325
}
412326

413327
Ok(Some(code))
414328
}
415329

416-
fn spawn_auth_code_reader(
417-
stdin_is_terminal: bool,
418-
auth_url: String,
419-
can_copy_url: bool,
420-
tx: mpsc::UnboundedSender<AuthInput>,
421-
) {
330+
fn spawn_auth_code_reader(tx: mpsc::UnboundedSender<AuthInput>) {
422331
thread::spawn(move || loop {
423-
match read_auth_code(stdin_is_terminal, &auth_url, can_copy_url) {
332+
match read_auth_code() {
424333
Ok(Some(code)) => {
425334
let _ = tx.send(AuthInput::Code {
426335
code,
@@ -443,8 +352,6 @@ async fn handle_auth_input(
443352
state: &str,
444353
code_verifier: &str,
445354
stdin_is_terminal: bool,
446-
manual_auth_url: &str,
447-
can_copy_url: bool,
448355
tx: &mpsc::UnboundedSender<AuthInput>,
449356
success_url: &str,
450357
) -> Result<Option<LoginResult>> {
@@ -465,12 +372,7 @@ async fn handle_auth_input(
465372
}
466373
Err(error) if matches!(source, AuthCodeSource::Prompt) && stdin_is_terminal => {
467374
eprintln!("Invalid auth code: {error}");
468-
spawn_auth_code_reader(
469-
stdin_is_terminal,
470-
manual_auth_url.to_string(),
471-
can_copy_url,
472-
tx.clone(),
473-
);
375+
spawn_auth_code_reader(tx.clone());
474376
Ok(None)
475377
}
476378
Err(error) if matches!(source, AuthCodeSource::Callback) => {
@@ -494,16 +396,6 @@ async fn handle_auth_input(
494396
}
495397
}
496398

497-
async fn recv_auth_input(rx: &mut mpsc::UnboundedReceiver<AuthInput>) -> Result<Option<AuthInput>> {
498-
tokio::select! {
499-
input = rx.recv() => Ok(input),
500-
result = signal::ctrl_c() => {
501-
result?;
502-
bail!("Authentication cancelled")
503-
}
504-
}
505-
}
506-
507399
async fn redeem_auth_code(code: String, state: &str, code_verifier: &str) -> Result<LoginResult> {
508400
let api_host = config::get("api_host")?;
509401
let url = format!("{}/cli/auth/redeem", api_host);
@@ -549,38 +441,40 @@ pub async fn login_with_browser() -> Result<LoginResult> {
549441
let success_url = format!("{website_host}/auth/cli?success=true");
550442

551443
let stdin_is_terminal = io::stdin().is_terminal();
552-
let can_copy_url = stdin_is_terminal && can_copy_to_clipboard();
553444
print_opening_browser();
554-
print_auth_url(&manual_auth_url, can_copy_url);
555-
if stdin_is_terminal {
556-
spawn_auth_code_reader(
557-
stdin_is_terminal,
558-
manual_auth_url.clone(),
559-
can_copy_url,
560-
tx.clone(),
561-
);
562-
}
563-
445+
print_auth_url(&manual_auth_url);
564446
let _ = open::that(&auto_auth_url);
447+
let mut auth_code_prompt_started = false;
565448

566-
while let Some(input) = recv_auth_input(&mut rx).await? {
567-
if let Some(result) = handle_auth_input(
568-
input,
569-
&state,
570-
&code_verifier,
571-
stdin_is_terminal,
572-
&manual_auth_url,
573-
can_copy_url,
574-
&tx,
575-
&success_url,
576-
)
577-
.await?
578-
{
579-
return Ok(result);
449+
loop {
450+
tokio::select! {
451+
input = rx.recv() => {
452+
let Some(input) = input else {
453+
bail!("Authentication cancelled");
454+
};
455+
if let Some(result) = handle_auth_input(
456+
input,
457+
&state,
458+
&code_verifier,
459+
stdin_is_terminal,
460+
&tx,
461+
&success_url,
462+
)
463+
.await?
464+
{
465+
return Ok(result);
466+
}
467+
}
468+
result = signal::ctrl_c() => {
469+
result?;
470+
bail!("Authentication cancelled")
471+
}
472+
_ = sleep(AUTH_CODE_PROMPT_DELAY), if stdin_is_terminal && !auth_code_prompt_started => {
473+
spawn_auth_code_reader(tx.clone());
474+
auth_code_prompt_started = true;
475+
}
580476
}
581477
}
582-
583-
bail!("Authentication cancelled")
584478
}
585479

586480
#[cfg(test)]

0 commit comments

Comments
 (0)