steer_tui/tui/widgets/setup/
authentication.rs

1use crate::tui::state::SetupState;
2use crate::tui::theme::{Component, Theme};
3use ratatui::{
4    buffer::Buffer,
5    layout::{Alignment, Constraint, Direction, Layout, Rect},
6    text::{Line, Span},
7    widgets::{Block, Borders, Paragraph, Widget, Wrap},
8};
9use steer_core::api::ProviderKind;
10
11pub struct AuthenticationWidget;
12
13impl AuthenticationWidget {
14    pub fn render(
15        area: Rect,
16        buf: &mut Buffer,
17        state: &SetupState,
18        provider: ProviderKind,
19        theme: &Theme,
20    ) {
21        let chunks = Layout::default()
22            .direction(Direction::Vertical)
23            .constraints([
24                Constraint::Length(4), // Header
25                Constraint::Min(10),   // Main content
26                Constraint::Length(3), // Error message
27                Constraint::Length(3), // Instructions
28            ])
29            .split(area);
30
31        // Header
32        let provider_name = provider.display_name();
33
34        let header = vec![
35            Line::from(""),
36            Line::from(Span::styled(
37                format!("Authenticate with {provider_name}"),
38                theme.style(Component::SetupHeader),
39            )),
40        ];
41
42        Paragraph::new(header)
43            .block(
44                Block::default()
45                    .borders(Borders::TOP | Borders::LEFT | Borders::RIGHT)
46                    .border_style(theme.style(Component::SetupBorder)),
47            )
48            .alignment(Alignment::Center)
49            .render(chunks[0], buf);
50
51        // Main content
52        let mut content = vec![];
53
54        if let Some(oauth_state) = &state.oauth_state {
55            // OAuth flow in progress
56            content.push(Line::from(""));
57            content.push(Line::from(Span::styled(
58                "OAuth Authentication",
59                theme.style(Component::SetupHeader),
60            )));
61            content.push(Line::from(""));
62
63            if state.oauth_callback_input.is_empty() {
64                content.push(Line::from("Please visit this URL in your browser:"));
65                content.push(Line::from(""));
66                content.push(Line::from(Span::styled(
67                    &oauth_state.auth_url,
68                    theme.style(Component::SetupUrl),
69                )));
70                content.push(Line::from(""));
71                content.push(Line::from(
72                    "After authorizing, you'll be redirected to a page showing a code.",
73                ));
74                content.push(Line::from(
75                    "Copy the ENTIRE code (including the part after the #)",
76                ));
77                content.push(Line::from("and paste it below:"));
78                content.push(Line::from(""));
79                content.push(Line::from(vec![
80                    Span::styled("Code: ", theme.style(Component::SetupInputLabel)),
81                    Span::styled(
82                        if state.oauth_callback_input.is_empty() {
83                            "_"
84                        } else {
85                            &state.oauth_callback_input
86                        },
87                        theme.style(Component::SetupInputValue),
88                    ),
89                ]));
90            } else {
91                content.push(Line::from("Processing authorization code..."));
92                content.push(Line::from(""));
93                content.push(Line::from(vec![
94                    Span::styled("Code: ", theme.style(Component::SetupInputLabel)),
95                    Span::styled(
96                        &state.oauth_callback_input,
97                        theme.style(Component::SetupInputValue),
98                    ),
99                ]));
100            }
101        } else if provider == ProviderKind::Anthropic
102            && state.api_key_input.is_empty()
103            && state.oauth_state.is_none()
104            && state.auth_providers.get(&provider)
105                != Some(&crate::tui::state::AuthStatus::InProgress)
106        {
107            // Anthropic - show auth options
108            content.push(Line::from(""));
109            content.push(Line::from("Choose authentication method:"));
110            content.push(Line::from(""));
111            content.push(Line::from(vec![
112                Span::styled("1. ", theme.style(Component::SetupKeyBinding)),
113                Span::raw("OAuth Login "),
114                Span::styled(
115                    "(Recommended for Claude Pro users)",
116                    theme.style(Component::SetupHint),
117                ),
118            ]));
119            content.push(Line::from(vec![
120                Span::styled("2. ", theme.style(Component::SetupKeyBinding)),
121                Span::raw("API Key"),
122            ]));
123        } else {
124            // API key input
125            content.push(Line::from(""));
126            content.push(Line::from(format!("Enter your {provider_name} API key:")));
127            content.push(Line::from(""));
128
129            let masked_key = if state.api_key_input.is_empty() {
130                String::from("_")
131            } else {
132                "*".repeat(state.api_key_input.len())
133            };
134
135            content.push(Line::from(vec![
136                Span::styled("API Key: ", theme.style(Component::SetupInputLabel)),
137                Span::styled(masked_key, theme.style(Component::SetupInputValue)),
138            ]));
139
140            if provider == ProviderKind::Anthropic {
141                content.push(Line::from(""));
142                content.push(Line::from(Span::styled(
143                    "Tip: Get your API key from console.anthropic.com",
144                    theme.style(Component::SetupHint),
145                )));
146            }
147        }
148
149        Paragraph::new(content)
150            .block(
151                Block::default()
152                    .borders(Borders::LEFT | Borders::RIGHT)
153                    .border_style(theme.style(Component::SetupBorder)),
154            )
155            .alignment(Alignment::Left)
156            .wrap(Wrap { trim: true })
157            .render(chunks[1], buf);
158
159        // Error message
160        if let Some(error) = &state.error_message {
161            let error_text = vec![Line::from(Span::styled(
162                format!("Error: {error}"),
163                theme.style(Component::SetupErrorMessage),
164            ))];
165
166            Paragraph::new(error_text)
167                .block(
168                    Block::default()
169                        .borders(Borders::LEFT | Borders::RIGHT)
170                        .border_style(theme.style(Component::SetupBorder)),
171                )
172                .alignment(Alignment::Center)
173                .render(chunks[2], buf);
174        }
175
176        // Instructions
177        let instructions = if state.oauth_state.is_some() {
178            vec![Line::from(vec![
179                Span::styled("Esc", theme.style(Component::SetupKeyBinding)),
180                Span::raw(" to cancel"),
181            ])]
182        } else if provider == ProviderKind::Anthropic && state.api_key_input.is_empty() {
183            vec![Line::from(vec![
184                Span::raw("Press "),
185                Span::styled("1", theme.style(Component::SetupKeyBinding)),
186                Span::raw(" or "),
187                Span::styled("2", theme.style(Component::SetupKeyBinding)),
188                Span::raw(" to select, "),
189                Span::styled("Esc", theme.style(Component::SetupKeyBinding)),
190                Span::raw(" to go back"),
191            ])]
192        } else {
193            vec![Line::from(vec![
194                Span::raw("Type your API key, "),
195                Span::styled("Enter", theme.style(Component::SetupKeyBinding)),
196                Span::raw(" to submit, "),
197                Span::styled("Esc", theme.style(Component::SetupKeyBinding)),
198                Span::raw(" to go back"),
199            ])]
200        };
201
202        Paragraph::new(instructions)
203            .block(
204                Block::default()
205                    .borders(Borders::BOTTOM | Borders::LEFT | Borders::RIGHT)
206                    .border_style(theme.style(Component::SetupBorder)),
207            )
208            .alignment(Alignment::Center)
209            .render(chunks[3], buf);
210    }
211}