cc-switch-tui 0.1.3

All-in-One Assistant for Claude Code, Codex, Gemini, OpenCode, OpenClaw & Hermes
Documentation
use crate::proxy::providers::codex_oauth_auth::CodexOAuthError;
use crate::services::CodexOAuthService;

const AUTH_PROVIDER_CODEX_OAUTH: &str = "codex_oauth";

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
pub struct ManagedAuthAccount {
    pub id: String,
    pub provider: String,
    pub login: String,
    pub avatar_url: Option<String>,
    pub authenticated_at: i64,
    pub is_default: bool,
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
pub struct ManagedAuthStatus {
    pub provider: String,
    pub authenticated: bool,
    pub default_account_id: Option<String>,
    pub migration_error: Option<String>,
    pub accounts: Vec<ManagedAuthAccount>,
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
pub struct ManagedAuthDeviceCodeResponse {
    pub provider: String,
    pub device_code: String,
    pub user_code: String,
    pub verification_uri: String,
    pub expires_in: u64,
    pub interval: u64,
}

fn ensure_auth_provider(auth_provider: &str) -> Result<&'static str, String> {
    match auth_provider {
        AUTH_PROVIDER_CODEX_OAUTH => Ok(AUTH_PROVIDER_CODEX_OAUTH),
        _ => Err(format!("Unsupported auth provider: {auth_provider}")),
    }
}

fn map_account(
    provider: &str,
    account: crate::proxy::providers::codex_oauth_auth::ManagedAuthAccount,
    default_account_id: Option<&str>,
) -> ManagedAuthAccount {
    ManagedAuthAccount {
        is_default: default_account_id == Some(account.id.as_str()),
        id: account.id,
        provider: provider.to_string(),
        login: account.login,
        avatar_url: account.avatar_url,
        authenticated_at: account.authenticated_at,
    }
}

fn map_device_code_response(
    provider: &str,
    response: crate::proxy::providers::codex_oauth_auth::ManagedAuthDeviceCodeResponse,
) -> ManagedAuthDeviceCodeResponse {
    ManagedAuthDeviceCodeResponse {
        provider: provider.to_string(),
        device_code: response.device_code,
        user_code: response.user_code,
        verification_uri: response.verification_uri,
        expires_in: response.expires_in,
        interval: response.interval,
    }
}

pub struct AuthService;

impl AuthService {
    pub async fn start_login(auth_provider: &str) -> Result<ManagedAuthDeviceCodeResponse, String> {
        let auth_provider = ensure_auth_provider(auth_provider)?;
        match auth_provider {
            AUTH_PROVIDER_CODEX_OAUTH => CodexOAuthService::start_device_flow()
                .await
                .map(|response| map_device_code_response(auth_provider, response))
                .map_err(|error| error.to_string()),
            _ => unreachable!(),
        }
    }

    pub async fn poll_for_account(
        auth_provider: &str,
        device_code: &str,
    ) -> Result<Option<ManagedAuthAccount>, String> {
        let auth_provider = ensure_auth_provider(auth_provider)?;
        match auth_provider {
            AUTH_PROVIDER_CODEX_OAUTH => match CodexOAuthService::poll_for_token(device_code).await
            {
                Ok(account) => {
                    let default_account_id =
                        CodexOAuthService::get_status().await.default_account_id;
                    Ok(account.map(|account| {
                        map_account(auth_provider, account, default_account_id.as_deref())
                    }))
                }
                Err(CodexOAuthError::AuthorizationPending) => Ok(None),
                Err(error) => Err(error.to_string()),
            },
            _ => unreachable!(),
        }
    }

    pub async fn list_accounts(auth_provider: &str) -> Result<Vec<ManagedAuthAccount>, String> {
        let auth_provider = ensure_auth_provider(auth_provider)?;
        match auth_provider {
            AUTH_PROVIDER_CODEX_OAUTH => {
                let status = CodexOAuthService::get_status().await;
                let default_account_id = status.default_account_id.clone();
                Ok(status
                    .accounts
                    .into_iter()
                    .map(|account| {
                        map_account(auth_provider, account, default_account_id.as_deref())
                    })
                    .collect())
            }
            _ => unreachable!(),
        }
    }

    pub async fn get_status(auth_provider: &str) -> Result<ManagedAuthStatus, String> {
        let auth_provider = ensure_auth_provider(auth_provider)?;
        match auth_provider {
            AUTH_PROVIDER_CODEX_OAUTH => {
                let status = CodexOAuthService::get_status().await;
                let default_account_id = status.default_account_id.clone();
                Ok(ManagedAuthStatus {
                    provider: auth_provider.to_string(),
                    authenticated: status.authenticated,
                    default_account_id: default_account_id.clone(),
                    migration_error: None,
                    accounts: status
                        .accounts
                        .into_iter()
                        .map(|account| {
                            map_account(auth_provider, account, default_account_id.as_deref())
                        })
                        .collect(),
                })
            }
            _ => unreachable!(),
        }
    }

    pub async fn remove_account(auth_provider: &str, account_id: &str) -> Result<(), String> {
        let auth_provider = ensure_auth_provider(auth_provider)?;
        match auth_provider {
            AUTH_PROVIDER_CODEX_OAUTH => CodexOAuthService::remove_account(account_id)
                .await
                .map_err(|error| error.to_string()),
            _ => unreachable!(),
        }
    }

    pub async fn set_default_account(auth_provider: &str, account_id: &str) -> Result<(), String> {
        let auth_provider = ensure_auth_provider(auth_provider)?;
        match auth_provider {
            AUTH_PROVIDER_CODEX_OAUTH => CodexOAuthService::set_default_account(account_id)
                .await
                .map_err(|error| error.to_string()),
            _ => unreachable!(),
        }
    }

    pub async fn logout(auth_provider: &str) -> Result<(), String> {
        let auth_provider = ensure_auth_provider(auth_provider)?;
        match auth_provider {
            AUTH_PROVIDER_CODEX_OAUTH => CodexOAuthService::clear_auth()
                .await
                .map_err(|error| error.to_string()),
            _ => unreachable!(),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::test_support::{lock_codex_oauth_test, lock_test_home_and_settings};
    use std::{env, ffi::OsString};

    struct ConfigDirEnvGuard {
        original: Option<OsString>,
    }

    impl ConfigDirEnvGuard {
        fn set(value: Option<&str>) -> Self {
            let original = env::var_os("CC_SWITCH_CONFIG_DIR");
            match value {
                Some(value) => unsafe { env::set_var("CC_SWITCH_CONFIG_DIR", value) },
                None => unsafe { env::remove_var("CC_SWITCH_CONFIG_DIR") },
            }
            Self { original }
        }
    }

    impl Drop for ConfigDirEnvGuard {
        fn drop(&mut self) {
            match self.original.as_ref() {
                Some(value) => unsafe { env::set_var("CC_SWITCH_CONFIG_DIR", value) },
                None => unsafe { env::remove_var("CC_SWITCH_CONFIG_DIR") },
            }
        }
    }

    #[tokio::test(flavor = "current_thread")]
    async fn auth_status_marks_default_account() {
        let _codex_lock = lock_codex_oauth_test();
        let _lock = lock_test_home_and_settings();
        let temp = tempfile::tempdir().expect("create temp dir");
        let _guard = ConfigDirEnvGuard::set(Some(temp.path().to_string_lossy().as_ref()));
        CodexOAuthService::reset_for_tests();

        CodexOAuthService::seed_account_for_tests(
            "acc-123",
            "rt-1",
            Some("a@example.com"),
            Some("at-1"),
            None,
        )
        .await
        .expect("seed first account");
        CodexOAuthService::seed_account_for_tests(
            "acc-456",
            "rt-2",
            Some("b@example.com"),
            Some("at-2"),
            None,
        )
        .await
        .expect("seed second account");
        AuthService::set_default_account("codex_oauth", "acc-456")
            .await
            .expect("set default account");

        let status = AuthService::get_status("codex_oauth")
            .await
            .expect("get auth status");

        assert_eq!(status.provider, "codex_oauth");
        assert!(status.authenticated);
        assert_eq!(status.default_account_id.as_deref(), Some("acc-456"));
        assert_eq!(status.accounts.len(), 2);
        assert_eq!(status.accounts[0].id, "acc-456");
        assert!(status.accounts[0].is_default);
        assert!(!status.accounts[1].is_default);
    }
}