SSH keys in account settings; wrappers for agents, terminal and git

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Sonnet 5.5 committed 2026-11-15 11:47:31 +01:00
1 parent 247ddcf14a
commit 6e8e739906
16 files changed
+517 -6

No files matched your search

+53
View File
@@ -157,6 +157,8 @@ pub fn router(app: AppState) -> Router {
.route("/images/{id}", delete(delete_image))
.route("/images/{id}/file", get(image_file))
.route("/account/git", get(get_git_settings).put(put_git_settings))
.route("/account/ssh", get(get_ssh_keys).post(add_ssh_key))
.route("/account/ssh/{id}", delete(delete_ssh_key))
.route("/account/snippets", get(crate::cli_api::get_snippets).put(crate::cli_api::put_snippets))
.route("/account/prefs", get(crate::cli_api::get_prefs).put(crate::cli_api::put_prefs))
.route("/account/sidebar", get(crate::cli_api::get_sidebar).put(crate::cli_api::put_sidebar))
@@ -2426,6 +2428,57 @@ async fn run_job(Extension(user): Extension<User>, State(app): State<AppState>,
// --- git credentials ---------------------------------------------------------------
// --- SSH keys -----------------------------------------------------------------
async fn get_ssh_keys(Extension(user): Extension<User>, State(app): State<AppState>) -> ApiResult {
let s: crate::sshkeys::SshSettings = app.store.user_setting(&user.id, "ssh")?;
Ok(Json(json!(crate::sshkeys::masked(&s))))
}
#[derive(Deserialize)]
struct NewSshKey {
name: String,
/// An existing private key to import; a new key is generated when missing.
private: Option<String>,
}
async fn add_ssh_key(Extension(user): Extension<User>, State(app): State<AppState>, Json(b): Json<NewSshKey>) -> ApiResult {
let name = b.name.trim().to_string();
if name.is_empty() || name.chars().count() > 60 {
return Err(ApiError(StatusCode::BAD_REQUEST, "give the key a name (up to 60 characters)".into()));
}
let mut s: crate::sshkeys::SshSettings = app.store.user_setting(&user.id, "ssh")?;
if s.keys.len() >= 10 {
return Err(ApiError(StatusCode::BAD_REQUEST, "at most 10 keys".into()));
}
let key = match b.private.as_deref().filter(|p| !p.trim().is_empty()) {
Some(p) => crate::sshkeys::import(&name, p).await,
None => crate::sshkeys::generate(&name).await,
}
.map_err(|e| ApiError(StatusCode::BAD_REQUEST, format!("{e:#}")))?;
if s.keys.iter().any(|k| k.fingerprint == key.fingerprint && !key.fingerprint.is_empty()) {
return Err(ApiError(StatusCode::CONFLICT, "this key is already stored".into()));
}
s.keys.push(key);
crate::sshkeys::write_files(&app, &user, &s).map_err(|e| ApiError(StatusCode::INTERNAL_SERVER_ERROR, format!("{e:#}")))?;
app.store.set_user_setting(&user.id, "ssh", &s)?;
app.audit(Some(&user), "ssh.key.add", &name, "");
Ok(Json(json!(crate::sshkeys::masked(&s))))
}
async fn delete_ssh_key(Extension(user): Extension<User>, State(app): State<AppState>, Path(id): Path<String>) -> ApiResult {
let mut s: crate::sshkeys::SshSettings = app.store.user_setting(&user.id, "ssh")?;
let before = s.keys.len();
s.keys.retain(|k| k.id != id);
if s.keys.len() == before {
return Err(not_found("key"));
}
crate::sshkeys::write_files(&app, &user, &s).map_err(|e| ApiError(StatusCode::INTERNAL_SERVER_ERROR, format!("{e:#}")))?;
app.store.set_user_setting(&user.id, "ssh", &s)?;
app.audit(Some(&user), "ssh.key.delete", &id, "");
Ok(Json(json!(crate::sshkeys::masked(&s))))
}
async fn get_git_settings(Extension(user): Extension<User>, State(app): State<AppState>) -> ApiResult {
let s: crate::gitcreds::GitSettings = app.store.user_setting(&user.id, "git")?;
Ok(Json(json!(crate::gitcreds::masked(&s))))