Skip to main content

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_grpc::client_api::{AuthProgress, ProviderId, provider};
10
11pub struct AuthenticationWidget;
12
13impl AuthenticationWidget {
14    pub fn render(
15        area: Rect,
16        buf: &mut Buffer,
17        state: &SetupState,
18        provider_id: ProviderId,
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_config = state.registry.get(&provider_id);
33        let provider_name = provider_config.map_or("Unknown Provider", |c| c.name.as_str());
34        let is_openai = provider_id == provider::openai();
35
36        let header = vec![
37            Line::from(""),
38            Line::from(Span::styled(
39                format!("Authenticate with {provider_name}"),
40                theme.style(Component::SetupHeader),
41            )),
42        ];
43
44        Paragraph::new(header)
45            .block(
46                Block::default()
47                    .borders(Borders::TOP | Borders::LEFT | Borders::RIGHT)
48                    .border_style(theme.style(Component::SetupBorder)),
49            )
50            .alignment(Alignment::Center)
51            .render(chunks[0], buf);
52
53        // Main content
54        let mut content = vec![];
55
56        match state.auth_progress.as_ref() {
57            Some(AuthProgress::OAuthStarted { auth_url }) => {
58                content.push(Line::from(""));
59                content.push(Line::from(Span::styled(
60                    "OAuth Authentication",
61                    theme.style(Component::SetupHeader),
62                )));
63                content.push(Line::from(""));
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                    auth_url,
68                    theme.style(Component::SetupUrl),
69                )));
70                content.push(Line::from(""));
71                if is_openai {
72                    content.push(Line::from(
73                        "After authorizing, you'll be redirected to http://localhost:1455/auth/callback.",
74                    ));
75                    content.push(Line::from(
76                        "If nothing happens, copy the full URL from your browser",
77                    ));
78                    content.push(Line::from("and paste it below:"));
79                } else {
80                    content.push(Line::from(
81                        "After authorizing, you'll be redirected to a page showing a code.",
82                    ));
83                    content.push(Line::from(
84                        "Copy the full URL or the code (including the part after the #)",
85                    ));
86                    content.push(Line::from("and paste it below:"));
87                }
88                content.push(Line::from(""));
89                content.push(Line::from(vec![
90                    Span::styled("Callback: ", theme.style(Component::SetupInputLabel)),
91                    Span::styled(
92                        if state.auth_input.is_empty() {
93                            "_"
94                        } else {
95                            &state.auth_input
96                        },
97                        theme.style(Component::SetupInputValue),
98                    ),
99                ]));
100            }
101            Some(AuthProgress::NeedInput { prompt }) => {
102                content.push(Line::from(""));
103                content.push(Line::from(prompt.clone()));
104                content.push(Line::from(""));
105
106                let masked_input = if state.auth_input.is_empty() {
107                    String::from("_")
108                } else {
109                    "*".repeat(state.auth_input.len())
110                };
111
112                content.push(Line::from(vec![
113                    Span::styled("Input: ", theme.style(Component::SetupInputLabel)),
114                    Span::styled(masked_input, theme.style(Component::SetupInputValue)),
115                ]));
116
117                if provider_id == provider::anthropic() {
118                    content.push(Line::from(""));
119                    content.push(Line::from(Span::styled(
120                        "Tip: Get your API key from console.anthropic.com",
121                        theme.style(Component::SetupHint),
122                    )));
123                }
124            }
125            Some(AuthProgress::InProgress { message }) => {
126                content.push(Line::from(""));
127                content.push(Line::from(message.clone()));
128            }
129            Some(AuthProgress::Complete) => {
130                content.push(Line::from(""));
131                content.push(Line::from("Authentication complete."));
132            }
133            Some(AuthProgress::Error { message }) => {
134                content.push(Line::from(""));
135                content.push(Line::from(format!("Error: {message}")));
136            }
137            None => {
138                content.push(Line::from(""));
139                content.push(Line::from("Starting authentication..."));
140            }
141        }
142
143        Paragraph::new(content)
144            .block(
145                Block::default()
146                    .borders(Borders::LEFT | Borders::RIGHT)
147                    .border_style(theme.style(Component::SetupBorder)),
148            )
149            .alignment(Alignment::Left)
150            .wrap(Wrap { trim: true })
151            .render(chunks[1], buf);
152
153        // Error message
154        if let Some(error) = &state.error_message {
155            let error_text = vec![Line::from(Span::styled(
156                format!("Error: {error}"),
157                theme.style(Component::SetupErrorMessage),
158            ))];
159
160            Paragraph::new(error_text)
161                .block(
162                    Block::default()
163                        .borders(Borders::LEFT | Borders::RIGHT)
164                        .border_style(theme.style(Component::SetupBorder)),
165                )
166                .alignment(Alignment::Center)
167                .render(chunks[2], buf);
168        }
169
170        // Instructions
171        let instructions = match state.auth_progress.as_ref() {
172            Some(AuthProgress::OAuthStarted { .. } | AuthProgress::NeedInput { .. }) => {
173                vec![Line::from(vec![
174                    Span::raw("Type or paste input, "),
175                    Span::styled("Enter", theme.style(Component::SetupKeyBinding)),
176                    Span::raw(" to submit, "),
177                    Span::styled("Esc", theme.style(Component::SetupKeyBinding)),
178                    Span::raw(" to cancel"),
179                ])]
180            }
181            _ => vec![Line::from(vec![
182                Span::styled("Esc", theme.style(Component::SetupKeyBinding)),
183                Span::raw(" to go back"),
184            ])],
185        };
186
187        Paragraph::new(instructions)
188            .block(
189                Block::default()
190                    .borders(Borders::BOTTOM | Borders::LEFT | Borders::RIGHT)
191                    .border_style(theme.style(Component::SetupBorder)),
192            )
193            .alignment(Alignment::Center)
194            .render(chunks[3], buf);
195    }
196}