mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 21:30:17 +08:00
87635e7fc6
* feat(copilot): add GitHub Enterprise Server support * fix(copilot): address GHES PR review findings (P1 + 2×P2) - P1: Use composite account ID (domain:user_id) for GHES to prevent cross-instance ID collisions; github.com keeps plain numeric ID for backward compatibilit - P2-a: Use get_api_endpoint() for model list URL with automatic fallback to static URL when dynamic endpoint resolution fails - P2-b: Add normalize_github_domain() as backend SSOT for domain normalization (lowercase, strip protocol/path/query, reject userinfo) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
306 lines
11 KiB
Rust
306 lines
11 KiB
Rust
use tauri::State;
|
|
|
|
use crate::commands::codex_oauth::CodexOAuthState;
|
|
use crate::commands::copilot::CopilotAuthState;
|
|
use crate::proxy::providers::codex_oauth_auth::CodexOAuthError;
|
|
use crate::proxy::providers::copilot_auth::{
|
|
CopilotAuthError, GitHubAccount, GitHubDeviceCodeResponse,
|
|
};
|
|
|
|
const AUTH_PROVIDER_GITHUB_COPILOT: &str = "github_copilot";
|
|
const AUTH_PROVIDER_CODEX_OAUTH: &str = "codex_oauth";
|
|
|
|
#[derive(Debug, Clone, serde::Serialize)]
|
|
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,
|
|
pub github_domain: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, serde::Serialize)]
|
|
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)]
|
|
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_GITHUB_COPILOT => Ok(AUTH_PROVIDER_GITHUB_COPILOT),
|
|
AUTH_PROVIDER_CODEX_OAUTH => Ok(AUTH_PROVIDER_CODEX_OAUTH),
|
|
_ => Err(format!("Unsupported auth provider: {auth_provider}")),
|
|
}
|
|
}
|
|
|
|
fn map_account(
|
|
provider: &str,
|
|
account: GitHubAccount,
|
|
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,
|
|
github_domain: account.github_domain,
|
|
}
|
|
}
|
|
|
|
fn map_device_code_response(
|
|
provider: &str,
|
|
response: GitHubDeviceCodeResponse,
|
|
) -> 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,
|
|
}
|
|
}
|
|
|
|
#[tauri::command(rename_all = "camelCase")]
|
|
pub async fn auth_start_login(
|
|
auth_provider: String,
|
|
github_domain: Option<String>,
|
|
copilot_state: State<'_, CopilotAuthState>,
|
|
codex_state: State<'_, CodexOAuthState>,
|
|
) -> Result<ManagedAuthDeviceCodeResponse, String> {
|
|
let auth_provider = ensure_auth_provider(&auth_provider)?;
|
|
match auth_provider {
|
|
AUTH_PROVIDER_GITHUB_COPILOT => {
|
|
let auth_manager = copilot_state.0.read().await;
|
|
let response = auth_manager
|
|
.start_device_flow(github_domain.as_deref())
|
|
.await
|
|
.map_err(|e| e.to_string())?;
|
|
Ok(map_device_code_response(auth_provider, response))
|
|
}
|
|
AUTH_PROVIDER_CODEX_OAUTH => {
|
|
let auth_manager = codex_state.0.read().await;
|
|
let response = auth_manager
|
|
.start_device_flow()
|
|
.await
|
|
.map_err(|e| e.to_string())?;
|
|
Ok(map_device_code_response(auth_provider, response))
|
|
}
|
|
_ => unreachable!(),
|
|
}
|
|
}
|
|
|
|
#[tauri::command(rename_all = "camelCase")]
|
|
pub async fn auth_poll_for_account(
|
|
auth_provider: String,
|
|
device_code: String,
|
|
github_domain: Option<String>,
|
|
copilot_state: State<'_, CopilotAuthState>,
|
|
codex_state: State<'_, CodexOAuthState>,
|
|
) -> Result<Option<ManagedAuthAccount>, String> {
|
|
let auth_provider = ensure_auth_provider(&auth_provider)?;
|
|
match auth_provider {
|
|
AUTH_PROVIDER_GITHUB_COPILOT => {
|
|
let auth_manager = copilot_state.0.write().await;
|
|
match auth_manager
|
|
.poll_for_token(&device_code, github_domain.as_deref())
|
|
.await
|
|
{
|
|
Ok(account) => {
|
|
let default_account_id = auth_manager.get_status().await.default_account_id;
|
|
Ok(account.map(|account| {
|
|
map_account(auth_provider, account, default_account_id.as_deref())
|
|
}))
|
|
}
|
|
Err(CopilotAuthError::AuthorizationPending) => Ok(None),
|
|
Err(e) => Err(e.to_string()),
|
|
}
|
|
}
|
|
AUTH_PROVIDER_CODEX_OAUTH => {
|
|
let auth_manager = codex_state.0.write().await;
|
|
match auth_manager.poll_for_token(&device_code).await {
|
|
Ok(account) => {
|
|
let default_account_id = auth_manager.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(e) => Err(e.to_string()),
|
|
}
|
|
}
|
|
_ => unreachable!(),
|
|
}
|
|
}
|
|
|
|
#[tauri::command(rename_all = "camelCase")]
|
|
pub async fn auth_list_accounts(
|
|
auth_provider: String,
|
|
copilot_state: State<'_, CopilotAuthState>,
|
|
codex_state: State<'_, CodexOAuthState>,
|
|
) -> Result<Vec<ManagedAuthAccount>, String> {
|
|
let auth_provider = ensure_auth_provider(&auth_provider)?;
|
|
match auth_provider {
|
|
AUTH_PROVIDER_GITHUB_COPILOT => {
|
|
let auth_manager = copilot_state.0.read().await;
|
|
let status = auth_manager.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())
|
|
}
|
|
AUTH_PROVIDER_CODEX_OAUTH => {
|
|
let auth_manager = codex_state.0.read().await;
|
|
let status = auth_manager.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!(),
|
|
}
|
|
}
|
|
|
|
#[tauri::command(rename_all = "camelCase")]
|
|
pub async fn auth_get_status(
|
|
auth_provider: String,
|
|
copilot_state: State<'_, CopilotAuthState>,
|
|
codex_state: State<'_, CodexOAuthState>,
|
|
) -> Result<ManagedAuthStatus, String> {
|
|
let auth_provider = ensure_auth_provider(&auth_provider)?;
|
|
match auth_provider {
|
|
AUTH_PROVIDER_GITHUB_COPILOT => {
|
|
let auth_manager = copilot_state.0.read().await;
|
|
let status = auth_manager.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: status.migration_error,
|
|
accounts: status
|
|
.accounts
|
|
.into_iter()
|
|
.map(|account| {
|
|
map_account(auth_provider, account, default_account_id.as_deref())
|
|
})
|
|
.collect(),
|
|
})
|
|
}
|
|
AUTH_PROVIDER_CODEX_OAUTH => {
|
|
let auth_manager = codex_state.0.read().await;
|
|
let status = auth_manager.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!(),
|
|
}
|
|
}
|
|
|
|
#[tauri::command(rename_all = "camelCase")]
|
|
pub async fn auth_remove_account(
|
|
auth_provider: String,
|
|
account_id: String,
|
|
copilot_state: State<'_, CopilotAuthState>,
|
|
codex_state: State<'_, CodexOAuthState>,
|
|
) -> Result<(), String> {
|
|
let auth_provider = ensure_auth_provider(&auth_provider)?;
|
|
match auth_provider {
|
|
AUTH_PROVIDER_GITHUB_COPILOT => {
|
|
let auth_manager = copilot_state.0.write().await;
|
|
auth_manager
|
|
.remove_account(&account_id)
|
|
.await
|
|
.map_err(|e| e.to_string())
|
|
}
|
|
AUTH_PROVIDER_CODEX_OAUTH => {
|
|
let auth_manager = codex_state.0.write().await;
|
|
auth_manager
|
|
.remove_account(&account_id)
|
|
.await
|
|
.map_err(|e| e.to_string())
|
|
}
|
|
_ => unreachable!(),
|
|
}
|
|
}
|
|
|
|
#[tauri::command(rename_all = "camelCase")]
|
|
pub async fn auth_set_default_account(
|
|
auth_provider: String,
|
|
account_id: String,
|
|
copilot_state: State<'_, CopilotAuthState>,
|
|
codex_state: State<'_, CodexOAuthState>,
|
|
) -> Result<(), String> {
|
|
let auth_provider = ensure_auth_provider(&auth_provider)?;
|
|
match auth_provider {
|
|
AUTH_PROVIDER_GITHUB_COPILOT => {
|
|
let auth_manager = copilot_state.0.write().await;
|
|
auth_manager
|
|
.set_default_account(&account_id)
|
|
.await
|
|
.map_err(|e| e.to_string())
|
|
}
|
|
AUTH_PROVIDER_CODEX_OAUTH => {
|
|
let auth_manager = codex_state.0.write().await;
|
|
auth_manager
|
|
.set_default_account(&account_id)
|
|
.await
|
|
.map_err(|e| e.to_string())
|
|
}
|
|
_ => unreachable!(),
|
|
}
|
|
}
|
|
|
|
#[tauri::command(rename_all = "camelCase")]
|
|
pub async fn auth_logout(
|
|
auth_provider: String,
|
|
copilot_state: State<'_, CopilotAuthState>,
|
|
codex_state: State<'_, CodexOAuthState>,
|
|
) -> Result<(), String> {
|
|
let auth_provider = ensure_auth_provider(&auth_provider)?;
|
|
match auth_provider {
|
|
AUTH_PROVIDER_GITHUB_COPILOT => {
|
|
let auth_manager = copilot_state.0.write().await;
|
|
auth_manager.clear_auth().await.map_err(|e| e.to_string())
|
|
}
|
|
AUTH_PROVIDER_CODEX_OAUTH => {
|
|
let auth_manager = codex_state.0.write().await;
|
|
auth_manager.clear_auth().await.map_err(|e| e.to_string())
|
|
}
|
|
_ => unreachable!(),
|
|
}
|
|
}
|