steer_tui/tui/widgets/setup/
authentication.rs1use 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::config::provider::{self, ProviderId};
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), Constraint::Min(10), Constraint::Length(3), Constraint::Length(3), ])
29 .split(area);
30
31 let provider_config = state.registry.get(&provider_id);
33 let provider_name = provider_config
34 .map(|c| c.name.as_str())
35 .unwrap_or("Unknown Provider");
36 let supports_oauth = provider_config
37 .map(|c| {
38 c.auth_schemes
39 .contains(&steer_grpc::proto::ProviderAuthScheme::AuthSchemeOauth2)
40 })
41 .unwrap_or(false);
42
43 let header = vec![
44 Line::from(""),
45 Line::from(Span::styled(
46 format!("Authenticate with {provider_name}"),
47 theme.style(Component::SetupHeader),
48 )),
49 ];
50
51 Paragraph::new(header)
52 .block(
53 Block::default()
54 .borders(Borders::TOP | Borders::LEFT | Borders::RIGHT)
55 .border_style(theme.style(Component::SetupBorder)),
56 )
57 .alignment(Alignment::Center)
58 .render(chunks[0], buf);
59
60 let mut content = vec![];
62
63 if let Some(oauth_state) = &state.oauth_state {
64 content.push(Line::from(""));
66 content.push(Line::from(Span::styled(
67 "OAuth Authentication",
68 theme.style(Component::SetupHeader),
69 )));
70 content.push(Line::from(""));
71
72 if state.oauth_callback_input.is_empty() {
73 content.push(Line::from("Please visit this URL in your browser:"));
74 content.push(Line::from(""));
75 content.push(Line::from(Span::styled(
76 &oauth_state.auth_url,
77 theme.style(Component::SetupUrl),
78 )));
79 content.push(Line::from(""));
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 ENTIRE code (including the part after the #)",
85 ));
86 content.push(Line::from("and paste it below:"));
87 content.push(Line::from(""));
88 content.push(Line::from(vec![
89 Span::styled("Code: ", theme.style(Component::SetupInputLabel)),
90 Span::styled(
91 if state.oauth_callback_input.is_empty() {
92 "_"
93 } else {
94 &state.oauth_callback_input
95 },
96 theme.style(Component::SetupInputValue),
97 ),
98 ]));
99 } else {
100 content.push(Line::from("Processing authorization code..."));
101 content.push(Line::from(""));
102 content.push(Line::from(vec![
103 Span::styled("Code: ", theme.style(Component::SetupInputLabel)),
104 Span::styled(
105 &state.oauth_callback_input,
106 theme.style(Component::SetupInputValue),
107 ),
108 ]));
109 }
110 } else if supports_oauth
111 && provider_id == provider::anthropic()
112 && state.api_key_input.is_empty()
113 && state.oauth_state.is_none()
114 && state.auth_providers.get(&provider_id)
115 != Some(&crate::tui::state::AuthStatus::InProgress)
116 {
117 content.push(Line::from(""));
119 content.push(Line::from("Choose authentication method:"));
120 content.push(Line::from(""));
121 content.push(Line::from(vec![
122 Span::styled("1. ", theme.style(Component::SetupKeyBinding)),
123 Span::raw("OAuth Login "),
124 Span::styled(
125 "(Recommended for Claude Pro users)",
126 theme.style(Component::SetupHint),
127 ),
128 ]));
129 content.push(Line::from(vec![
130 Span::styled("2. ", theme.style(Component::SetupKeyBinding)),
131 Span::raw("API Key"),
132 ]));
133 } else {
134 content.push(Line::from(""));
136 content.push(Line::from(format!("Enter your {provider_name} API key:")));
137 content.push(Line::from(""));
138
139 let masked_key = if state.api_key_input.is_empty() {
140 String::from("_")
141 } else {
142 "*".repeat(state.api_key_input.len())
143 };
144
145 content.push(Line::from(vec![
146 Span::styled("API Key: ", theme.style(Component::SetupInputLabel)),
147 Span::styled(masked_key, theme.style(Component::SetupInputValue)),
148 ]));
149
150 if provider_id == provider::anthropic() {
151 content.push(Line::from(""));
152 content.push(Line::from(Span::styled(
153 "Tip: Get your API key from console.anthropic.com",
154 theme.style(Component::SetupHint),
155 )));
156 }
157 }
158
159 Paragraph::new(content)
160 .block(
161 Block::default()
162 .borders(Borders::LEFT | Borders::RIGHT)
163 .border_style(theme.style(Component::SetupBorder)),
164 )
165 .alignment(Alignment::Left)
166 .wrap(Wrap { trim: true })
167 .render(chunks[1], buf);
168
169 if let Some(error) = &state.error_message {
171 let error_text = vec![Line::from(Span::styled(
172 format!("Error: {error}"),
173 theme.style(Component::SetupErrorMessage),
174 ))];
175
176 Paragraph::new(error_text)
177 .block(
178 Block::default()
179 .borders(Borders::LEFT | Borders::RIGHT)
180 .border_style(theme.style(Component::SetupBorder)),
181 )
182 .alignment(Alignment::Center)
183 .render(chunks[2], buf);
184 }
185
186 let instructions = if state.oauth_state.is_some() {
188 vec![Line::from(vec![
189 Span::styled("Esc", theme.style(Component::SetupKeyBinding)),
190 Span::raw(" to cancel"),
191 ])]
192 } else if supports_oauth
193 && provider_id == provider::anthropic()
194 && state.api_key_input.is_empty()
195 {
196 vec![Line::from(vec![
197 Span::raw("Press "),
198 Span::styled("1", theme.style(Component::SetupKeyBinding)),
199 Span::raw(" or "),
200 Span::styled("2", theme.style(Component::SetupKeyBinding)),
201 Span::raw(" to select, "),
202 Span::styled("Esc", theme.style(Component::SetupKeyBinding)),
203 Span::raw(" to go back"),
204 ])]
205 } else {
206 vec![Line::from(vec![
207 Span::raw("Type your API key, "),
208 Span::styled("Enter", theme.style(Component::SetupKeyBinding)),
209 Span::raw(" to submit, "),
210 Span::styled("Esc", theme.style(Component::SetupKeyBinding)),
211 Span::raw(" to go back"),
212 ])]
213 };
214
215 Paragraph::new(instructions)
216 .block(
217 Block::default()
218 .borders(Borders::BOTTOM | Borders::LEFT | Borders::RIGHT)
219 .border_style(theme.style(Component::SetupBorder)),
220 )
221 .alignment(Alignment::Center)
222 .render(chunks[3], buf);
223 }
224}