Skip to main content

cc_switch_lib/services/
auth.rs

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}