fix: replace device flow with loopback ip redirect flow (RFC 8252)
- Device Flow only works with 'TV and Limited Input devices' OAuth client type - Desktop app type requires Authorization Code flow with localhost redirect - New flow: start local TCP server on random port, open browser with auth URL, catch redirect containing authorization code, exchange for tokens - Uses webbrowser crate to auto-open the browser - Self-contained: no separate HTTP server framework needed, uses std::net - Popup shows auth URL and waits for browser authorization - Support for refresh_token for long-lived access
This commit is contained in:
+76
-18
@@ -297,13 +297,15 @@ pub fn render_device_auth_popup(
|
||||
frame: &mut Frame,
|
||||
area: Rect,
|
||||
url: &str,
|
||||
code: &str,
|
||||
_code: &str,
|
||||
error: Option<&str>,
|
||||
) {
|
||||
let popup_area = centered_rect(75, 11, area);
|
||||
let popup_area = centered_rect(80, 13, area);
|
||||
|
||||
let border_color = if error.is_some() {
|
||||
Color::Red
|
||||
} else if url == "starting..." {
|
||||
Color::Yellow
|
||||
} else if url.is_empty() {
|
||||
POPUP_BORDER
|
||||
} else {
|
||||
@@ -314,7 +316,7 @@ pub fn render_device_auth_popup(
|
||||
.borders(Borders::ALL)
|
||||
.style(Style::default().bg(POPUP_BG))
|
||||
.border_style(Style::default().fg(border_color))
|
||||
.title(" Google OAuth ")
|
||||
.title(" Google Tasks - Authorization ")
|
||||
.title_alignment(Alignment::Left);
|
||||
|
||||
let mut lines = Vec::new();
|
||||
@@ -322,28 +324,52 @@ pub fn render_device_auth_popup(
|
||||
if let Some(err) = error {
|
||||
lines.push(Line::from(""));
|
||||
lines.push(Line::from(Span::styled(
|
||||
format!(" ERROR: {} ", err),
|
||||
" Authorization Error ",
|
||||
Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
|
||||
)));
|
||||
lines.push(Line::from(""));
|
||||
let wrapped = textwrap(&err, 50);
|
||||
for line in wrapped {
|
||||
lines.push(Line::from(Span::styled(
|
||||
line,
|
||||
Style::default().fg(Color::White),
|
||||
)));
|
||||
}
|
||||
lines.push(Line::from(""));
|
||||
lines.push(Line::from(Span::styled(
|
||||
" Press Enter to retry or Esc to cancel ",
|
||||
" Press Enter to retry | Esc to cancel ",
|
||||
Style::default().fg(Color::DarkGray),
|
||||
)));
|
||||
} else if url == "starting..." {
|
||||
lines.push(Line::from(""));
|
||||
lines.push(Line::from(Span::styled(
|
||||
" Starting authorization... ",
|
||||
Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD),
|
||||
)));
|
||||
lines.push(Line::from(""));
|
||||
lines.push(Line::from(Span::styled(
|
||||
" A browser tab will open or you can copy the URL manually. ",
|
||||
Style::default().fg(Color::DarkGray),
|
||||
)));
|
||||
} else if url.is_empty() {
|
||||
lines.push(Line::from(""));
|
||||
lines.push(Line::from(Span::styled(
|
||||
" Google Tasks Authorization Required ",
|
||||
" Google Tasks Authorization ",
|
||||
Style::default().fg(Color::White).add_modifier(Modifier::BOLD),
|
||||
)));
|
||||
lines.push(Line::from(""));
|
||||
lines.push(Line::from(Span::styled(
|
||||
" Set GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET env vars ",
|
||||
" This app needs access to your Google Tasks. ",
|
||||
Style::default().fg(Color::White),
|
||||
)));
|
||||
lines.push(Line::from(""));
|
||||
lines.push(Line::from(Span::styled(
|
||||
" Set env vars GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET ",
|
||||
Style::default().fg(Color::Yellow),
|
||||
)));
|
||||
lines.push(Line::from(""));
|
||||
lines.push(Line::from(Span::styled(
|
||||
" Press Enter to start authorization ",
|
||||
" Press Enter to start ",
|
||||
Style::default().fg(Color::Cyan),
|
||||
)));
|
||||
lines.push(Line::from(Span::styled(
|
||||
@@ -353,33 +379,65 @@ pub fn render_device_auth_popup(
|
||||
} else {
|
||||
lines.push(Line::from(""));
|
||||
lines.push(Line::from(Span::styled(
|
||||
" Visit the following URL to authorize: ",
|
||||
Style::default().fg(Color::White),
|
||||
" Authorize in your browser: ",
|
||||
Style::default().fg(Color::White).add_modifier(Modifier::BOLD),
|
||||
)));
|
||||
lines.push(Line::from(""));
|
||||
lines.push(Line::from(Span::styled(
|
||||
url,
|
||||
Style::default().fg(Color::Cyan).add_modifier(Modifier::UNDERLINED),
|
||||
)));
|
||||
// Show a shortened version of the URL for readability
|
||||
if url.len() > 70 {
|
||||
let short_url: String = url.chars().take(67).collect();
|
||||
lines.push(Line::from(Span::styled(
|
||||
format!(" {}", short_url),
|
||||
Style::default().fg(Color::Cyan),
|
||||
)));
|
||||
lines.push(Line::from(Span::styled(
|
||||
" ... (full URL in terminal log) ",
|
||||
Style::default().fg(Color::DarkGray),
|
||||
)));
|
||||
} else {
|
||||
lines.push(Line::from(Span::styled(
|
||||
format!(" {}", url),
|
||||
Style::default().fg(Color::Cyan),
|
||||
)));
|
||||
}
|
||||
lines.push(Line::from(""));
|
||||
lines.push(Line::from(Span::styled(
|
||||
format!(" Enter code: {} ", code),
|
||||
" Waiting for browser authorization... ",
|
||||
Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD),
|
||||
)));
|
||||
lines.push(Line::from(""));
|
||||
lines.push(Line::from(Span::styled(
|
||||
" Waiting for authorization... (Esc to cancel) ",
|
||||
" (Esc to cancel) ",
|
||||
Style::default().fg(Color::DarkGray),
|
||||
)));
|
||||
}
|
||||
|
||||
let paragraph = Paragraph::new(Text::from(lines))
|
||||
.block(block)
|
||||
.alignment(Alignment::Center);
|
||||
.alignment(Alignment::Left);
|
||||
|
||||
frame.render_widget(paragraph, popup_area);
|
||||
}
|
||||
|
||||
/// Simple word wrap: splits text at word boundaries to fit max_width chars per line
|
||||
fn textwrap(text: &str, max_width: usize) -> Vec<String> {
|
||||
let mut result = Vec::new();
|
||||
let mut current = String::new();
|
||||
for word in text.split_whitespace() {
|
||||
if current.len() + word.len() + 1 > max_width && !current.is_empty() {
|
||||
result.push(current.clone());
|
||||
current.clear();
|
||||
}
|
||||
if !current.is_empty() {
|
||||
current.push(' ');
|
||||
}
|
||||
current.push_str(word);
|
||||
}
|
||||
if !current.is_empty() {
|
||||
result.push(current);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn centered_rect(percent_x: u16, percent_y: u16, area: Rect) -> Rect {
|
||||
let popup_layout = ratatui::layout::Layout::default()
|
||||
.direction(ratatui::layout::Direction::Vertical)
|
||||
|
||||
Reference in New Issue
Block a user