1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
use anyhow::{Context, Result};
use fido_types::User;
use crate::api::{ApiError, Backend};
use crate::session::SessionStore;
/// Manages the OAuth authentication flow for the TUI client.
///
/// This struct handles:
/// - Checking for existing sessions
/// - Initiating GitHub OAuth flow
/// - Opening the system browser
/// - Polling for session completion
/// - Storing session tokens
pub struct AuthFlow {
api_client: Backend,
session_store: SessionStore,
}
impl AuthFlow {
/// Creates a new AuthFlow instance.
pub fn new(api_client: Backend) -> Result<Self> {
let session_store = match api_client.session_scope() {
Some(server_url) => SessionStore::for_server(server_url)
.context("Failed to initialize server-scoped session store")?,
None => SessionStore::new().context("Failed to initialize session store")?,
};
Ok(Self {
api_client,
session_store,
})
}
/// Checks for an existing session and validates it with the server.
///
/// # Returns
///
/// - `Ok(Some(user))` if a valid session exists
/// - `Ok(None)` if no session exists or the session is invalid
/// - `Err(_)` if there's an error communicating with the server
pub async fn check_existing_session(&mut self) -> Result<Option<User>> {
// Try to load session token from file
let token = match self.session_store.load()? {
Some(t) => t,
None => {
log::debug!("No existing session found");
return Ok(None);
}
};
log::info!("Found existing session token, validating with server");
// Set the session token in the API client
self.api_client.set_session_token(Some(token.clone()));
// Validate the session with the server
match self.api_client.validate_session().await {
Ok(response) if response.valid => {
log::info!("Session is valid for user: {}", response.user.username);
Ok(Some(response.user))
}
Ok(_) => {
log::warn!("Session validation returned invalid");
// Clear invalid session
let _ = self.session_store.delete();
Ok(None)
}
Err(ApiError::Unauthorized(_))
| Err(ApiError::NotFound(_))
| Err(ApiError::BadRequest(_)) => {
log::warn!("Session token rejected by server, clearing local token");
let _ = self.session_store.delete();
Ok(None)
}
Err(e) => {
// Keep the local token on transient failures (network/server/rate-limit).
// This prevents unnecessary re-authentication after temporary outages.
log::warn!(
"Session validation failed due to transient error; keeping local token: {}",
e
);
Ok(None)
}
}
}
/// Initiates the GitHub Device Flow by requesting a device code from the server.
///
/// # Returns
///
/// Returns a tuple of (device_code, user_code, verification_uri, interval) where:
/// - `device_code` is used for polling
/// - `user_code` is displayed to the user
/// - `verification_uri` is where the user enters the code
/// - `interval` is how often to poll (in seconds)
pub async fn initiate_github_device_flow(&self) -> Result<(String, String, String, i64)> {
log::info!("Initiating GitHub Device Flow");
let response = self
.api_client
.github_device_flow()
.await
.map_err(|e| anyhow::anyhow!("Failed to initiate GitHub Device Flow: {}", e))?;
log::debug!(
"Received device code with user code: {}",
response.user_code
);
Ok((
response.device_code,
response.user_code,
response.verification_uri,
response.interval,
))
}
/// Opens the system browser to the given URL.
///
/// # Arguments
///
/// * `url` - The URL to open in the browser
///
/// # Returns
///
/// Returns an error if the browser cannot be opened.
pub fn open_browser(&self, url: &str) -> Result<()> {
log::info!("Opening browser to: {}", url);
webbrowser::open(url).context("Failed to open browser")?;
Ok(())
}
/// Saves a session token to the session store.
///
/// This is useful when the session token is obtained through other means
/// (e.g., test user login).
pub fn save_session(&self, token: &str) -> Result<()> {
self.session_store
.save(token)
.context("Failed to save session token")
}
/// Gets a reference to the API client.
pub fn api_client(&self) -> &Backend {
&self.api_client
}
/// Gets a mutable reference to the API client.
pub fn api_client_mut(&mut self) -> &mut Backend {
&mut self.api_client
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_auth_flow_creation() {
let api_client = Backend::api("http://localhost:3000");
let auth_flow = AuthFlow::new(api_client);
assert!(auth_flow.is_ok());
}
}