From 450b081e88a9374e07d83dd87d9931a8408846bf Mon Sep 17 00:00:00 2001 From: xintaofei Date: Sat, 21 Mar 2026 18:00:05 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84git=E5=87=AD=E8=AF=81?= =?UTF-8?q?=E6=89=98=E7=AE=A1=EF=BC=8C=E6=94=B9=E4=B8=BA=E6=93=8D=E4=BD=9C?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F=E6=89=98=E7=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/Cargo.lock | 46 +++++++++++++++++++ src-tauri/Cargo.toml | 1 + src-tauri/src/commands/version_control.rs | 32 +++++++++++++ src-tauri/src/git_credential.rs | 30 ++++++++---- src-tauri/src/keyring_store.rs | 30 ++++++++++++ src-tauri/src/lib.rs | 4 ++ src-tauri/src/models/system.rs | 1 - .../settings/add-git-account-dialog.tsx | 11 ++++- .../settings/add-github-account-dialog.tsx | 4 +- .../settings/version-control-settings.tsx | 12 ++++- src/contexts/git-credential-context.tsx | 8 ++-- src/lib/tauri.ts | 19 ++++++++ src/lib/types.ts | 1 - 13 files changed, 180 insertions(+), 19 deletions(-) create mode 100644 src-tauri/src/keyring_store.rs diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index cdcf862..ae91dbc 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -802,6 +802,7 @@ dependencies = [ "fix-path-env", "flate2", "futures", + "keyring", "kill_tree", "notify", "portable-pty", @@ -1151,6 +1152,27 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "dbus" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b3aa68d7e7abee336255bd7248ea965cc393f3e70411135a6f6a4b651345d4" +dependencies = [ + "libc", + "libdbus-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "dbus-secret-service" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "708b509edf7889e53d7efb0ffadd994cc6c2345ccb62f55cfd6b0682165e4fa6" +dependencies = [ + "dbus", + "zeroize", +] + [[package]] name = "deflate64" version = "0.1.10" @@ -2716,6 +2738,21 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "keyring" +version = "3.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eebcc3aff044e5944a8fbaf69eb277d11986064cba30c468730e8b9909fb551c" +dependencies = [ + "byteorder", + "dbus-secret-service", + "log", + "security-framework 2.11.1", + "security-framework 3.5.1", + "windows-sys 0.60.2", + "zeroize", +] + [[package]] name = "kill_tree" version = "0.2.4" @@ -2806,6 +2843,15 @@ version = "0.2.180" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +[[package]] +name = "libdbus-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" +dependencies = [ + "pkg-config", +] + [[package]] name = "libloading" version = "0.7.4" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index bcc9e01..ed42c0c 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -49,6 +49,7 @@ base64 = "0.22" agent-client-protocol-schema = { version = "0.10", features = ["unstable_session_usage", "unstable_session_fork"] } kill_tree = { version = "0.2", features = ["tokio"] } which = "7" +keyring = { version = "3", features = ["apple-native", "windows-native", "sync-secret-service"] } [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] tauri-plugin-window-state = "2" diff --git a/src-tauri/src/commands/version_control.rs b/src-tauri/src/commands/version_control.rs index cacb420..a18addb 100644 --- a/src-tauri/src/commands/version_control.rs +++ b/src-tauri/src/commands/version_control.rs @@ -203,6 +203,38 @@ pub async fn update_github_accounts( Ok(settings) } +// --------------------------------------------------------------------------- +// Keyring token management +// --------------------------------------------------------------------------- + +#[tauri::command] +pub async fn save_account_token( + account_id: String, + token: String, +) -> Result<(), AppCommandError> { + crate::keyring_store::set_token(&account_id, &token) + .map_err(|e| AppCommandError::io_error("Failed to save token to keyring").with_detail(e)) +} + +#[tauri::command] +pub async fn get_account_token( + account_id: String, +) -> Result, AppCommandError> { + Ok(crate::keyring_store::get_token(&account_id)) +} + +#[tauri::command] +pub async fn delete_account_token( + account_id: String, +) -> Result<(), AppCommandError> { + crate::keyring_store::delete_token(&account_id) + .map_err(|e| AppCommandError::io_error("Failed to delete token from keyring").with_detail(e)) +} + +// --------------------------------------------------------------------------- +// GitHub token validation +// --------------------------------------------------------------------------- + #[derive(Debug, Deserialize)] struct GitHubUserResponse { login: String, diff --git a/src-tauri/src/git_credential.rs b/src-tauri/src/git_credential.rs index 8cd52b6..e7c07f1 100644 --- a/src-tauri/src/git_credential.rs +++ b/src-tauri/src/git_credential.rs @@ -123,8 +123,10 @@ pub fn run_credential_helper() { let remote_url = format!("https://{}", host); if let Some(account) = find_matching_account(&settings.accounts, &remote_url) { - println!("username={}", account.username); - println!("password={}", account.token); + if let Some(token) = crate::keyring_store::get_token(&account.id) { + println!("username={}", account.username); + println!("password={}", token); + } } }); } @@ -387,11 +389,19 @@ pub async fn try_inject_for_repo( } }; + let token = match crate::keyring_store::get_token(&account.id) { + Some(t) => t, + None => { + eprintln!("[GIT_CRED] no token in keyring for account {}", account.id); + return false; + } + }; + eprintln!( "[GIT_CRED] injecting credentials for {} (user: {})", remote_url, account.username ); - inject_credentials(cmd, &account.username, &account.token, &askpass); + inject_credentials(cmd, &account.username, &token, &askpass); true } @@ -417,6 +427,14 @@ pub async fn try_inject_for_url( None => return false, }; + let token = match crate::keyring_store::get_token(&account.id) { + Some(t) => t, + None => { + eprintln!("[GIT_CRED] no token in keyring for account {}", account.id); + return false; + } + }; + let askpass = match ensure_askpass_script(app_data_dir) { Ok(p) => p, Err(e) => { @@ -425,7 +443,7 @@ pub async fn try_inject_for_url( } }; - inject_credentials(cmd, &account.username, &account.token, &askpass); + inject_credentials(cmd, &account.username, &token, &askpass); true } @@ -464,7 +482,6 @@ mod tests { id: "1".into(), server_url: "https://github.com".into(), username: "user1".into(), - token: "tok1".into(), scopes: vec![], avatar_url: None, is_default: false, @@ -474,7 +491,6 @@ mod tests { id: "2".into(), server_url: "https://gitlab.example.com".into(), username: "user2".into(), - token: "tok2".into(), scopes: vec![], avatar_url: None, is_default: true, @@ -500,7 +516,6 @@ mod tests { id: "1".into(), server_url: "https://github.com".into(), username: "personal".into(), - token: "tok1".into(), scopes: vec![], avatar_url: None, is_default: false, @@ -510,7 +525,6 @@ mod tests { id: "2".into(), server_url: "https://github.com".into(), username: "work".into(), - token: "tok2".into(), scopes: vec![], avatar_url: None, is_default: true, diff --git a/src-tauri/src/keyring_store.rs b/src-tauri/src/keyring_store.rs new file mode 100644 index 0000000..dbdcfa2 --- /dev/null +++ b/src-tauri/src/keyring_store.rs @@ -0,0 +1,30 @@ +use keyring::Entry; + +const SERVICE_NAME: &str = "codeg"; + +fn token_key(account_id: &str) -> String { + format!("github-token:{}", account_id) +} + +pub fn set_token(account_id: &str, token: &str) -> Result<(), String> { + let entry = Entry::new(SERVICE_NAME, &token_key(account_id)) + .map_err(|e| format!("keyring init error: {e}"))?; + entry + .set_password(token) + .map_err(|e| format!("keyring set error: {e}")) +} + +pub fn get_token(account_id: &str) -> Option { + let entry = Entry::new(SERVICE_NAME, &token_key(account_id)).ok()?; + entry.get_password().ok() +} + +pub fn delete_token(account_id: &str) -> Result<(), String> { + let entry = Entry::new(SERVICE_NAME, &token_key(account_id)) + .map_err(|e| format!("keyring init error: {e}"))?; + match entry.delete_credential() { + Ok(()) => Ok(()), + Err(keyring::Error::NoEntry) => Ok(()), + Err(e) => Err(format!("keyring delete error: {e}")), + } +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 8116ff7..1f1de6b 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -3,6 +3,7 @@ mod app_error; mod commands; mod db; pub mod git_credential; +pub mod keyring_store; mod models; mod network; mod parsers; @@ -272,6 +273,9 @@ pub fn run() { version_control::get_github_accounts, version_control::validate_github_token, version_control::update_github_accounts, + version_control::save_account_token, + version_control::get_account_token, + version_control::delete_account_token, acp_commands::acp_preflight, acp_commands::acp_connect, acp_commands::acp_prompt, diff --git a/src-tauri/src/models/system.rs b/src-tauri/src/models/system.rs index 9fbc650..3d979cd 100644 --- a/src-tauri/src/models/system.rs +++ b/src-tauri/src/models/system.rs @@ -64,7 +64,6 @@ pub struct GitHubAccount { pub id: String, pub server_url: String, pub username: String, - pub token: String, pub scopes: Vec, pub avatar_url: Option, pub is_default: bool, diff --git a/src/components/settings/add-git-account-dialog.tsx b/src/components/settings/add-git-account-dialog.tsx index 27d9769..b9d56b7 100644 --- a/src/components/settings/add-git-account-dialog.tsx +++ b/src/components/settings/add-git-account-dialog.tsx @@ -13,6 +13,7 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog" +import { saveAccountToken } from "@/lib/tauri" import type { GitHubAccount } from "@/lib/types" interface AddGitAccountDialogProps { @@ -52,7 +53,7 @@ export function AddGitAccountDialog({ [onOpenChange, resetForm] ) - const handleSubmit = useCallback(() => { + const handleSubmit = useCallback(async () => { const trimmedUrl = serverUrl.trim() const trimmedUser = username.trim() const trimmedPass = password.trim() @@ -74,13 +75,19 @@ export function AddGitAccountDialog({ id: crypto.randomUUID(), server_url: trimmedUrl, username: trimmedUser, - token: trimmedPass, scopes: [], avatar_url: null, is_default: isFirstAccount, created_at: new Date().toISOString(), } + try { + await saveAccountToken(account.id, trimmedPass) + } catch { + setError(t("gitAccount.passwordRequired")) + return + } + onAccountAdded(account) handleOpenChange(false) }, [ diff --git a/src/components/settings/add-github-account-dialog.tsx b/src/components/settings/add-github-account-dialog.tsx index 7716d4d..6c432e9 100644 --- a/src/components/settings/add-github-account-dialog.tsx +++ b/src/components/settings/add-github-account-dialog.tsx @@ -14,7 +14,7 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog" -import { validateGitHubToken } from "@/lib/tauri" +import { validateGitHubToken, saveAccountToken } from "@/lib/tauri" import type { GitHubAccount } from "@/lib/types" interface AddGitHubAccountDialogProps { @@ -94,13 +94,13 @@ export function AddGitHubAccountDialog({ id: crypto.randomUUID(), server_url: serverUrl.trim() || "https://github.com", username: result.username ?? "unknown", - token: trimmedToken, scopes: result.scopes, avatar_url: result.avatar_url, is_default: isFirstAccount, created_at: new Date().toISOString(), } + await saveAccountToken(account.id, trimmedToken) onAccountAdded(account) handleOpenChange(false) } catch (err) { diff --git a/src/components/settings/version-control-settings.tsx b/src/components/settings/version-control-settings.tsx index 29abbe9..28ee85f 100644 --- a/src/components/settings/version-control-settings.tsx +++ b/src/components/settings/version-control-settings.tsx @@ -33,6 +33,8 @@ import { getGitHubAccounts, updateGitHubAccounts, validateGitHubToken, + getAccountToken, + deleteAccountToken, } from "@/lib/tauri" import type { GitDetectResult, @@ -260,10 +262,15 @@ export function VersionControlSettings() { async (account: GitHubAccount) => { setTestingAccountId(account.id) try { + const token = await getAccountToken(account.id) + if (!token) { + toast.error(t("connectionFailed", { message: "Token not found" })) + return + } if (isGitHubAccount(account)) { const result = await validateGitHubToken( account.server_url, - account.token + token ) if (result.success) { toast.success(t("connectionSuccess")) @@ -276,7 +283,7 @@ export function VersionControlSettings() { } } else { // For non-GitHub accounts we can't validate via API, - // just confirm the account is stored. + // just confirm the token exists in keyring. toast.success(t("connectionSuccess")) } } catch (err) { @@ -315,6 +322,7 @@ export function VersionControlSettings() { accounts: accounts.accounts.filter((a) => a.id !== removeTarget.id), } try { + await deleteAccountToken(removeTarget.id) const saved = await updateGitHubAccounts(updated) setAccounts(saved) toast.success(t("removeSuccess")) diff --git a/src/contexts/git-credential-context.tsx b/src/contexts/git-credential-context.tsx index abd29c0..97949f4 100644 --- a/src/contexts/git-credential-context.tsx +++ b/src/contexts/git-credential-context.tsx @@ -36,6 +36,7 @@ import { validateGitHubToken, getGitHubAccounts, updateGitHubAccounts, + saveAccountToken, } from "@/lib/tauri" // --------------------------------------------------------------------------- @@ -146,14 +147,15 @@ async function saveGenericAccount( (a) => a.username === creds.username && extractHost(a.server_url) === host ) if (!isDuplicate) { + const newId = crypto.randomUUID() + await saveAccountToken(newId, creds.password) await updateGitHubAccounts({ accounts: [ ...existing.accounts, { - id: crypto.randomUUID(), + id: newId, server_url: serverUrl, username: creds.username, - token: creds.password, scopes: [], avatar_url: null, is_default: existing.accounts.length === 0, @@ -284,12 +286,12 @@ export function GitCredentialProvider({ children }: { children: ReactNode }) { id: crypto.randomUUID(), server_url: serverUrl, username: result.username ?? "unknown", - token: trimmedToken, scopes: result.scopes, avatar_url: result.avatar_url, is_default: existing.accounts.length === 0, created_at: new Date().toISOString(), } + await saveAccountToken(newAccount.id, trimmedToken) await updateGitHubAccounts({ accounts: [...existing.accounts, newAccount], }) diff --git a/src/lib/tauri.ts b/src/lib/tauri.ts index 8ca985a..287606c 100644 --- a/src/lib/tauri.ts +++ b/src/lib/tauri.ts @@ -344,6 +344,25 @@ export async function updateGitHubAccounts( return invoke("update_github_accounts", { settings }) } +export async function saveAccountToken( + accountId: string, + token: string +): Promise { + return invoke("save_account_token", { accountId, token }) +} + +export async function getAccountToken( + accountId: string +): Promise { + return invoke("get_account_token", { accountId }) +} + +export async function deleteAccountToken( + accountId: string +): Promise { + return invoke("delete_account_token", { accountId }) +} + export async function mcpScanLocal(): Promise { return invoke("mcp_scan_local") } diff --git a/src/lib/types.ts b/src/lib/types.ts index 2cb7465..96339da 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -544,7 +544,6 @@ export interface GitHubAccount { id: string server_url: string username: string - token: string scopes: string[] avatar_url: string | null is_default: boolean