Add server editing and deletion

This commit is contained in:
Georg Bauer
2026-08-03 20:34:39 +02:00
parent 0f1d109af7
commit 9a0b179a0e
10 changed files with 664 additions and 40 deletions

View File

@@ -1,7 +1,14 @@
use std::{env, fs, path::PathBuf};
use std::{
env, fs,
path::PathBuf,
process,
time::{SystemTime, UNIX_EPOCH},
};
use gotcha_gitea::{Client, Provider};
use security_framework::passwords::{get_generic_password, set_generic_password};
use security_framework::passwords::{
delete_generic_password, get_generic_password, set_generic_password,
};
use crate::{
RepositoryPane,
@@ -40,6 +47,7 @@ pub fn validate_server(
name: name.into(),
url: url.into(),
provider,
credential_account: String::new(),
token: token.into(),
})
}
@@ -59,14 +67,19 @@ pub fn load_preferences() -> Result<Preferences, String> {
if !matches!(preferences.pull_status.as_str(), "open" | "closed") {
preferences.pull_status = open_status();
}
let mut credentials_migrated = false;
for server in &mut preferences.servers {
if server.credential_account.is_empty() {
server.credential_account = format!("{}|{}", server.name, server.url);
credentials_migrated = true;
}
server.token = String::from_utf8(
get_generic_password("de.rfc1437.gotcha", &keychain_account(server))
.map_err(|error| format!("Cannot read the token for {}: {error}", server.name))?,
)
.map_err(|_| format!("The token for {} is not valid text.", server.name))?;
}
if favorites_migrated {
if favorites_migrated || credentials_migrated {
save_preferences(&preferences)?;
}
Ok(preferences)
@@ -111,13 +124,54 @@ pub fn save_server_token(server: &Server) -> Result<(), String> {
.map_err(|error| format!("Cannot save the token for {}: {error}", server.name))
}
pub fn delete_server_token(server: &Server) -> Result<(), String> {
match delete_generic_password("de.rfc1437.gotcha", &keychain_account(server)) {
Ok(()) => Ok(()),
// A missing Keychain item must not make an otherwise valid profile undeletable.
Err(error) if error.code() == -25300 => Ok(()), // errSecItemNotFound
Err(error) => Err(format!(
"Cannot delete the token for {}: {error}",
server.name
)),
}
}
pub fn assign_server_credential_account(server: &mut Server, existing: &[Server]) {
if !server.credential_account.is_empty() {
return;
}
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos();
let prefix = format!("server-{}-{timestamp}", process::id());
server.credential_account = (0_u32..)
.map(|suffix| {
if suffix == 0 {
prefix.clone()
} else {
format!("{prefix}-{suffix}")
}
})
.find(|candidate| {
existing
.iter()
.all(|saved| saved.credential_account != *candidate)
})
.expect("a unique Keychain account must exist");
}
fn preferences_path() -> Result<PathBuf, String> {
let home = env::var_os("HOME").ok_or("Cannot find the app data directory.")?;
Ok(PathBuf::from(home).join("Library/Application Support/Gotcha/preferences.json"))
}
fn keychain_account(server: &Server) -> String {
format!("{}|{}", server.name, server.url)
if server.credential_account.is_empty() {
format!("{}|{}", server.name, server.url)
} else {
server.credential_account.clone()
}
}
#[cfg(test)]
@@ -182,6 +236,15 @@ mod tests {
let legacy_server: Server =
serde_json::from_str(r#"{"name":"Work","url":"https://gitea.example.com"}"#).unwrap();
assert_eq!(legacy_server.provider, Provider::Gitea);
assert!(legacy_server.credential_account.is_empty());
assert_eq!(
keychain_account(&legacy_server),
"Work|https://gitea.example.com"
);
let mut new_server = legacy_server.clone();
assign_server_credential_account(&mut new_server, &[]);
assert!(new_server.credential_account.starts_with("server-"));
assert_eq!(keychain_account(&new_server), new_server.credential_account);
assert_eq!(
crate::domain::AppearanceMode::from_index(1),
Some(crate::domain::AppearanceMode::Light)