1use crate::proxy::providers::codex_oauth_auth::CodexOAuthError;
2use crate::services::CodexOAuthService;
3
4const AUTH_PROVIDER_CODEX_OAUTH: &str = "codex_oauth";
5
6#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
7pub struct ManagedAuthAccount {
8 pub id: String,
9 pub provider: String,
10 pub login: String,
11 pub avatar_url: Option<String>,
12 pub authenticated_at: i64,
13 pub is_default: bool,
14}
15
16#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
17pub struct ManagedAuthStatus {
18 pub provider: String,
19 pub authenticated: bool,
20 pub default_account_id: Option<String>,
21 pub migration_error: Option<String>,
22 pub accounts: Vec<ManagedAuthAccount>,
23}
24
25#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
26pub struct ManagedAuthDeviceCodeResponse {
27 pub provider: String,
28 pub device_code: String,
29 pub user_code: String,
30 pub verification_uri: String,
31 pub expires_in: u64,
32 pub interval: u64,
33}
34
35fn ensure_auth_provider(auth_provider: &str) -> Result<&'static str, String> {
36 match auth_provider {
37 AUTH_PROVIDER_CODEX_OAUTH => Ok(AUTH_PROVIDER_CODEX_OAUTH),
38 _ => Err(format!("Unsupported auth provider: {auth_provider}")),
39 }
40}
41
42fn map_account(
43 provider: &str,
44 account: crate::proxy::providers::codex_oauth_auth::ManagedAuthAccount,
45 default_account_id: Option<&str>,
46) -> ManagedAuthAccount {
47 ManagedAuthAccount {
48 is_default: default_account_id == Some(account.id.as_str()),
49 id: account.id,
50 provider: provider.to_string(),
51 login: account.login,
52 avatar_url: account.avatar_url,
53 authenticated_at: account.authenticated_at,
54 }
55}
56
57fn map_device_code_response(
58 provider: &str,
59 response: crate::proxy::providers::codex_oauth_auth::ManagedAuthDeviceCodeResponse,
60) -> ManagedAuthDeviceCodeResponse {
61 ManagedAuthDeviceCodeResponse {
62 provider: provider.to_string(),
63 device_code: response.device_code,
64 user_code: response.user_code,
65 verification_uri: response.verification_uri,
66 expires_in: response.expires_in,
67 interval: response.interval,
68 }
69}
70
71pub struct AuthService;
72
73impl AuthService {
74 pub async fn start_login(auth_provider: &str) -> Result<ManagedAuthDeviceCodeResponse, String> {
75 let auth_provider = ensure_auth_provider(auth_provider)?;
76 match auth_provider {
77 AUTH_PROVIDER_CODEX_OAUTH => CodexOAuthService::start_device_flow()
78 .await
79 .map(|response| map_device_code_response(auth_provider, response))
80 .map_err(|error| error.to_string()),
81 _ => unreachable!(),
82 }
83 }
84
85 pub async fn poll_for_account(
86 auth_provider: &str,
87 device_code: &str,
88 ) -> Result<Option<ManagedAuthAccount>, String> {
89 let auth_provider = ensure_auth_provider(auth_provider)?;
90 match auth_provider {
91 AUTH_PROVIDER_CODEX_OAUTH => match CodexOAuthService::poll_for_token(device_code).await
92 {
93 Ok(account) => {
94 let default_account_id =
95 CodexOAuthService::get_status().await.default_account_id;
96 Ok(account.map(|account| {
97 map_account(auth_provider, account, default_account_id.as_deref())
98 }))
99 }
100 Err(CodexOAuthError::AuthorizationPending) => Ok(None),
101 Err(error) => Err(error.to_string()),
102 },
103 _ => unreachable!(),
104 }
105 }
106
107 pub async fn list_accounts(auth_provider: &str) -> Result<Vec<ManagedAuthAccount>, String> {
108 let auth_provider = ensure_auth_provider(auth_provider)?;
109 match auth_provider {
110 AUTH_PROVIDER_CODEX_OAUTH => {
111 let status = CodexOAuthService::get_status().await;
112 let default_account_id = status.default_account_id.clone();
113 Ok(status
114 .accounts
115 .into_iter()
116 .map(|account| {
117 map_account(auth_provider, account, default_account_id.as_deref())
118 })
119 .collect())
120 }
121 _ => unreachable!(),
122 }
123 }
124
125 pub async fn get_status(auth_provider: &str) -> Result<ManagedAuthStatus, String> {
126 let auth_provider = ensure_auth_provider(auth_provider)?;
127 match auth_provider {
128 AUTH_PROVIDER_CODEX_OAUTH => {
129 let status = CodexOAuthService::get_status().await;
130 let default_account_id = status.default_account_id.clone();
131 Ok(ManagedAuthStatus {
132 provider: auth_provider.to_string(),
133 authenticated: status.authenticated,
134 default_account_id: default_account_id.clone(),
135 migration_error: None,
136 accounts: status
137 .accounts
138 .into_iter()
139 .map(|account| {
140 map_account(auth_provider, account, default_account_id.as_deref())
141 })
142 .collect(),
143 })
144 }
145 _ => unreachable!(),
146 }
147 }
148
149 pub async fn remove_account(auth_provider: &str, account_id: &str) -> Result<(), String> {
150 let auth_provider = ensure_auth_provider(auth_provider)?;
151 match auth_provider {
152 AUTH_PROVIDER_CODEX_OAUTH => CodexOAuthService::remove_account(account_id)
153 .await
154 .map_err(|error| error.to_string()),
155 _ => unreachable!(),
156 }
157 }
158
159 pub async fn set_default_account(auth_provider: &str, account_id: &str) -> Result<(), String> {
160 let auth_provider = ensure_auth_provider(auth_provider)?;
161 match auth_provider {
162 AUTH_PROVIDER_CODEX_OAUTH => CodexOAuthService::set_default_account(account_id)
163 .await
164 .map_err(|error| error.to_string()),
165 _ => unreachable!(),
166 }
167 }
168
169 pub async fn logout(auth_provider: &str) -> Result<(), String> {
170 let auth_provider = ensure_auth_provider(auth_provider)?;
171 match auth_provider {
172 AUTH_PROVIDER_CODEX_OAUTH => CodexOAuthService::clear_auth()
173 .await
174 .map_err(|error| error.to_string()),
175 _ => unreachable!(),
176 }
177 }
178}
179
180#[cfg(test)]
181mod tests {
182 use super::*;
183 use crate::test_support::{lock_codex_oauth_test, lock_test_home_and_settings};
184 use std::{env, ffi::OsString};
185
186 struct ConfigDirEnvGuard {
187 original: Option<OsString>,
188 }
189
190 impl ConfigDirEnvGuard {
191 fn set(value: Option<&str>) -> Self {
192 let original = env::var_os("CC_SWITCH_CONFIG_DIR");
193 match value {
194 Some(value) => unsafe { env::set_var("CC_SWITCH_CONFIG_DIR", value) },
195 None => unsafe { env::remove_var("CC_SWITCH_CONFIG_DIR") },
196 }
197 Self { original }
198 }
199 }
200
201 impl Drop for ConfigDirEnvGuard {
202 fn drop(&mut self) {
203 match self.original.as_ref() {
204 Some(value) => unsafe { env::set_var("CC_SWITCH_CONFIG_DIR", value) },
205 None => unsafe { env::remove_var("CC_SWITCH_CONFIG_DIR") },
206 }
207 }
208 }
209
210 #[tokio::test(flavor = "current_thread")]
211 async fn auth_status_marks_default_account() {
212 let _codex_lock = lock_codex_oauth_test();
213 let _lock = lock_test_home_and_settings();
214 let temp = tempfile::tempdir().expect("create temp dir");
215 let _guard = ConfigDirEnvGuard::set(Some(temp.path().to_string_lossy().as_ref()));
216 CodexOAuthService::reset_for_tests();
217
218 CodexOAuthService::seed_account_for_tests(
219 "acc-123",
220 "rt-1",
221 Some("a@example.com"),
222 Some("at-1"),
223 None,
224 )
225 .await
226 .expect("seed first account");
227 CodexOAuthService::seed_account_for_tests(
228 "acc-456",
229 "rt-2",
230 Some("b@example.com"),
231 Some("at-2"),
232 None,
233 )
234 .await
235 .expect("seed second account");
236 AuthService::set_default_account("codex_oauth", "acc-456")
237 .await
238 .expect("set default account");
239
240 let status = AuthService::get_status("codex_oauth")
241 .await
242 .expect("get auth status");
243
244 assert_eq!(status.provider, "codex_oauth");
245 assert!(status.authenticated);
246 assert_eq!(status.default_account_id.as_deref(), Some("acc-456"));
247 assert_eq!(status.accounts.len(), 2);
248 assert_eq!(status.accounts[0].id, "acc-456");
249 assert!(status.accounts[0].is_default);
250 assert!(!status.accounts[1].is_default);
251 }
252}