add login with oskeychain
This commit is contained in:
Generated
+823
-3
File diff suppressed because it is too large
Load Diff
@@ -8,8 +8,10 @@ build = "build.rs"
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
tauri = { version = "2", features = [] }
|
||||
tauri-plugin-dialog = "=2.7.0"
|
||||
keyring = { version = "3", features = ["apple-native", "windows-native", "async-secret-service", "crypto-rust", "async-io"] }
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
+159
-1
@@ -1,4 +1,4 @@
|
||||
use serde::Serialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
ffi::{OsStr, OsString},
|
||||
@@ -365,6 +365,142 @@ pub fn push(
|
||||
status_for_repo(&repo)
|
||||
}
|
||||
|
||||
// ── Credential storage (OS keychain) ────────────────────────────────────────
|
||||
|
||||
const CRED_SERVICE: &str = "tauri_git_lite";
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct StoredCredential {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
#[serde(default, rename = "expiresAt", skip_serializing_if = "Option::is_none")]
|
||||
pub expires_at: Option<String>,
|
||||
}
|
||||
|
||||
fn cred_entry(key: &str) -> Result<keyring::Entry, String> {
|
||||
let key = key.trim();
|
||||
if key.is_empty() {
|
||||
return Err("Kein Schlüssel für die Zugangsdaten angegeben.".to_string());
|
||||
}
|
||||
keyring::Entry::new(CRED_SERVICE, key)
|
||||
.map_err(|err| format!("Schlüsselbund nicht verfügbar: {err}"))
|
||||
}
|
||||
|
||||
/// Returns the remote URL used for auth key derivation (upstream remote of the
|
||||
/// current branch, falling back to `origin`, then the first configured remote).
|
||||
#[tauri::command]
|
||||
pub fn get_remote_url(path: String) -> Result<Option<String>, String> {
|
||||
let repo = resolve_repo(&path)?;
|
||||
let remote = upstream_remote_name(&repo).unwrap_or_else(|| "origin".to_string());
|
||||
|
||||
if let Some(url) = remote_url_for(&repo, &remote) {
|
||||
return Ok(Some(url));
|
||||
}
|
||||
// origin missing → try the first configured remote
|
||||
if let Some(first) = first_remote_name(&repo) {
|
||||
if first != remote {
|
||||
if let Some(url) = remote_url_for(&repo, &first) {
|
||||
return Ok(Some(url));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn remote_url_for(repo: &Path, remote: &str) -> Option<String> {
|
||||
let out = Command::new("git")
|
||||
.arg("-C")
|
||||
.arg(repo)
|
||||
.args(["remote", "get-url", remote])
|
||||
.output()
|
||||
.ok()?;
|
||||
if !out.status.success() {
|
||||
return None;
|
||||
}
|
||||
let url = String::from_utf8_lossy(&out.stdout).trim().to_string();
|
||||
if url.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(url)
|
||||
}
|
||||
}
|
||||
|
||||
fn upstream_remote_name(repo: &Path) -> Option<String> {
|
||||
let branch = run_git(repo, ["rev-parse", "--abbrev-ref", "HEAD"]).ok()?;
|
||||
let branch = String::from_utf8_lossy(&branch).trim().to_string();
|
||||
if branch.is_empty() || branch == "HEAD" {
|
||||
return None;
|
||||
}
|
||||
let out = Command::new("git")
|
||||
.arg("-C")
|
||||
.arg(repo)
|
||||
.args(["config", &format!("branch.{branch}.remote")])
|
||||
.output()
|
||||
.ok()?;
|
||||
if !out.status.success() {
|
||||
return None;
|
||||
}
|
||||
let name = String::from_utf8_lossy(&out.stdout).trim().to_string();
|
||||
if name.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(name)
|
||||
}
|
||||
}
|
||||
|
||||
fn first_remote_name(repo: &Path) -> Option<String> {
|
||||
let out = run_git(repo, ["remote"]).ok()?;
|
||||
String::from_utf8_lossy(&out)
|
||||
.lines()
|
||||
.map(str::trim)
|
||||
.find(|line| !line.is_empty())
|
||||
.map(str::to_string)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn cred_load(key: String) -> Result<Option<StoredCredential>, String> {
|
||||
let entry = cred_entry(&key)?;
|
||||
match entry.get_password() {
|
||||
Ok(json) => {
|
||||
let cred = serde_json::from_str::<StoredCredential>(&json)
|
||||
.map_err(|err| format!("Gespeicherte Zugangsdaten unlesbar: {err}"))?;
|
||||
Ok(Some(cred))
|
||||
}
|
||||
Err(keyring::Error::NoEntry) => Ok(None),
|
||||
Err(err) => Err(format!("Schlüsselbund-Zugriff fehlgeschlagen: {err}")),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn cred_save(
|
||||
key: String,
|
||||
username: String,
|
||||
password: String,
|
||||
expires_at: Option<String>,
|
||||
) -> Result<(), String> {
|
||||
let entry = cred_entry(&key)?;
|
||||
let expires_at = expires_at.filter(|value| !value.trim().is_empty());
|
||||
let cred = StoredCredential {
|
||||
username,
|
||||
password,
|
||||
expires_at,
|
||||
};
|
||||
let json = serde_json::to_string(&cred)
|
||||
.map_err(|err| format!("Zugangsdaten konnten nicht serialisiert werden: {err}"))?;
|
||||
entry
|
||||
.set_password(&json)
|
||||
.map_err(|err| format!("Speichern im Schlüsselbund fehlgeschlagen: {err}"))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn cred_delete(key: String) -> Result<(), String> {
|
||||
let entry = cred_entry(&key)?;
|
||||
match entry.delete_credential() {
|
||||
Ok(()) | Err(keyring::Error::NoEntry) => Ok(()),
|
||||
Err(err) => Err(format!("Löschen im Schlüsselbund fehlgeschlagen: {err}")),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn merge_branch(path: String, branch: String) -> Result<GitStatus, String> {
|
||||
let repo = resolve_repo(&path)?;
|
||||
@@ -1824,9 +1960,31 @@ where
|
||||
} else {
|
||||
"Unbekannter Fehler".to_string()
|
||||
};
|
||||
if is_auth_error(&details) {
|
||||
return Err(format!("AUTH_FAILED:{details}"));
|
||||
}
|
||||
Err(format!("Git-Befehl fehlgeschlagen: {details}"))
|
||||
}
|
||||
|
||||
/// Heuristic: did git fail because the credentials were rejected/expired,
|
||||
/// as opposed to a network or merge error? Used by the frontend to drop the
|
||||
/// stored credential and re-prompt the login.
|
||||
fn is_auth_error(details: &str) -> bool {
|
||||
let d = details.to_lowercase();
|
||||
d.contains("authentication failed")
|
||||
|| d.contains("could not read username")
|
||||
|| d.contains("could not read password")
|
||||
|| d.contains("invalid username or password")
|
||||
|| d.contains("terminal prompts disabled")
|
||||
|| d.contains("permission denied")
|
||||
|| d.contains("access denied")
|
||||
|| d.contains("403 forbidden")
|
||||
|| d.contains(" 403")
|
||||
|| d.contains(" 401")
|
||||
|| d.contains("authorization failed")
|
||||
|| d.contains("authentication required")
|
||||
}
|
||||
|
||||
fn run_git_with_paths(
|
||||
repo: &Path,
|
||||
base_args: &[&str],
|
||||
|
||||
+10
-6
@@ -4,11 +4,11 @@ mod git;
|
||||
|
||||
use git::{
|
||||
cancel_code_search, checkout_branch, commit, compare_commits, compare_file_to_head,
|
||||
diff_file_against_working_tree, get_status, list_branches, list_commits, list_file_history,
|
||||
list_repository_files, merge_branch, open_repository, pull, push, read_conflict,
|
||||
resolve_conflict, resolve_conflict_side, restore_file_from_commit, restore_files,
|
||||
restore_to_commit, search_code_introductions, stage_files, unstage_files,
|
||||
SearchCancellationState,
|
||||
cred_delete, cred_load, cred_save, diff_file_against_working_tree, get_remote_url, get_status,
|
||||
list_branches, list_commits, list_file_history, list_repository_files, merge_branch,
|
||||
open_repository, pull, push, read_conflict, resolve_conflict, resolve_conflict_side,
|
||||
restore_file_from_commit, restore_files, restore_to_commit, search_code_introductions,
|
||||
stage_files, unstage_files, SearchCancellationState,
|
||||
};
|
||||
|
||||
fn main() {
|
||||
@@ -39,7 +39,11 @@ fn main() {
|
||||
cancel_code_search,
|
||||
read_conflict,
|
||||
resolve_conflict,
|
||||
resolve_conflict_side
|
||||
resolve_conflict_side,
|
||||
get_remote_url,
|
||||
cred_load,
|
||||
cred_save,
|
||||
cred_delete
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
||||
Reference in New Issue
Block a user