feat: TuleapCredentials model + encrypted storage + Tauri commands
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
9a451061ea
commit
3cf28babab
5 changed files with 251 additions and 0 deletions
80
src-tauri/src/commands/credential.rs
Normal file
80
src-tauri/src/commands/credential.rs
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
use crate::error::AppError;
|
||||
use crate::models::credential::{TuleapCredentials, TuleapCredentialsSafe};
|
||||
use crate::services::crypto;
|
||||
use crate::AppState;
|
||||
use tauri::State;
|
||||
|
||||
#[tauri::command]
|
||||
pub fn set_tuleap_credentials(
|
||||
state: State<'_, AppState>,
|
||||
tuleap_url: String,
|
||||
username: String,
|
||||
password: String,
|
||||
) -> Result<TuleapCredentialsSafe, AppError> {
|
||||
let password_encrypted = crypto::encrypt(&state.encryption_key, &password)
|
||||
.map_err(AppError::from)?;
|
||||
|
||||
let db = state
|
||||
.db
|
||||
.lock()
|
||||
.map_err(|e| AppError::from(format!("Database lock failed: {}", e)))?;
|
||||
|
||||
let creds = TuleapCredentials::upsert(&db, &tuleap_url, &username, &password_encrypted)?;
|
||||
Ok(creds.to_safe())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_tuleap_credentials(
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Option<TuleapCredentialsSafe>, AppError> {
|
||||
let db = state
|
||||
.db
|
||||
.lock()
|
||||
.map_err(|e| AppError::from(format!("Database lock failed: {}", e)))?;
|
||||
|
||||
let result = TuleapCredentials::get(&db)?.map(|c| c.to_safe());
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn delete_tuleap_credentials(state: State<'_, AppState>) -> Result<(), AppError> {
|
||||
let db = state
|
||||
.db
|
||||
.lock()
|
||||
.map_err(|e| AppError::from(format!("Database lock failed: {}", e)))?;
|
||||
|
||||
TuleapCredentials::delete(&db)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn test_tuleap_connection(state: State<'_, AppState>) -> Result<String, AppError> {
|
||||
let (tuleap_url, username, password) = {
|
||||
let db = state
|
||||
.db
|
||||
.lock()
|
||||
.map_err(|e| AppError::from(format!("Database lock failed: {}", e)))?;
|
||||
|
||||
let creds = TuleapCredentials::get(&db)?
|
||||
.ok_or_else(|| AppError::from("No credentials configured".to_string()))?;
|
||||
|
||||
let password = crypto::decrypt(&state.encryption_key, &creds.password_encrypted)
|
||||
.map_err(AppError::from)?;
|
||||
|
||||
(creds.tuleap_url, creds.username, password)
|
||||
};
|
||||
|
||||
let url = format!("{}/api/projects?limit=1", tuleap_url.trim_end_matches('/'));
|
||||
|
||||
state
|
||||
.http_client
|
||||
.get(&url)
|
||||
.basic_auth(&username, Some(&password))
|
||||
.send()
|
||||
.await
|
||||
.map_err(AppError::from)?
|
||||
.error_for_status()
|
||||
.map_err(AppError::from)?;
|
||||
|
||||
Ok("Connection successful".to_string())
|
||||
}
|
||||
|
|
@ -1 +1,2 @@
|
|||
pub mod credential;
|
||||
pub mod project;
|
||||
|
|
|
|||
|
|
@ -42,6 +42,10 @@ pub fn run() {
|
|||
commands::project::get_project,
|
||||
commands::project::update_project,
|
||||
commands::project::delete_project,
|
||||
commands::credential::set_tuleap_credentials,
|
||||
commands::credential::get_tuleap_credentials,
|
||||
commands::credential::delete_tuleap_credentials,
|
||||
commands::credential::test_tuleap_connection,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
|
|
|||
165
src-tauri/src/models/credential.rs
Normal file
165
src-tauri/src/models/credential.rs
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
use rusqlite::{params, Connection, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TuleapCredentials {
|
||||
pub id: String,
|
||||
pub tuleap_url: String,
|
||||
pub username: String,
|
||||
pub password_encrypted: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TuleapCredentialsSafe {
|
||||
pub id: String,
|
||||
pub tuleap_url: String,
|
||||
pub username: String,
|
||||
}
|
||||
|
||||
impl TuleapCredentials {
|
||||
pub fn upsert(
|
||||
conn: &Connection,
|
||||
tuleap_url: &str,
|
||||
username: &str,
|
||||
password_encrypted: &str,
|
||||
) -> Result<TuleapCredentials> {
|
||||
conn.execute("DELETE FROM tuleap_credentials", [])?;
|
||||
|
||||
let id = Uuid::new_v4().to_string();
|
||||
conn.execute(
|
||||
"INSERT INTO tuleap_credentials (id, tuleap_url, username, password_encrypted) VALUES (?1, ?2, ?3, ?4)",
|
||||
params![id, tuleap_url, username, password_encrypted],
|
||||
)?;
|
||||
|
||||
Ok(TuleapCredentials {
|
||||
id,
|
||||
tuleap_url: tuleap_url.to_string(),
|
||||
username: username.to_string(),
|
||||
password_encrypted: password_encrypted.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get(conn: &Connection) -> Result<Option<TuleapCredentials>> {
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT id, tuleap_url, username, password_encrypted FROM tuleap_credentials LIMIT 1",
|
||||
)?;
|
||||
let mut rows = stmt.query_map([], |row| {
|
||||
Ok(TuleapCredentials {
|
||||
id: row.get(0)?,
|
||||
tuleap_url: row.get(1)?,
|
||||
username: row.get(2)?,
|
||||
password_encrypted: row.get(3)?,
|
||||
})
|
||||
})?;
|
||||
|
||||
match rows.next() {
|
||||
Some(row) => Ok(Some(row?)),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delete(conn: &Connection) -> Result<()> {
|
||||
conn.execute("DELETE FROM tuleap_credentials", [])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn to_safe(&self) -> TuleapCredentialsSafe {
|
||||
TuleapCredentialsSafe {
|
||||
id: self.id.clone(),
|
||||
tuleap_url: self.tuleap_url.clone(),
|
||||
username: self.username.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::db;
|
||||
|
||||
fn setup() -> Connection {
|
||||
db::init_in_memory().expect("db init should succeed")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_upsert_creates_credentials() {
|
||||
let conn = setup();
|
||||
let creds = TuleapCredentials::upsert(
|
||||
&conn,
|
||||
"https://tuleap.example.com",
|
||||
"alice",
|
||||
"encrypted_password",
|
||||
)
|
||||
.expect("upsert should succeed");
|
||||
|
||||
assert_eq!(creds.tuleap_url, "https://tuleap.example.com");
|
||||
assert_eq!(creds.username, "alice");
|
||||
assert_eq!(creds.password_encrypted, "encrypted_password");
|
||||
assert!(!creds.id.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_upsert_replaces_existing() {
|
||||
let conn = setup();
|
||||
TuleapCredentials::upsert(&conn, "https://old.example.com", "old_user", "old_enc")
|
||||
.expect("first upsert should succeed");
|
||||
|
||||
let second = TuleapCredentials::upsert(
|
||||
&conn,
|
||||
"https://new.example.com",
|
||||
"new_user",
|
||||
"new_enc",
|
||||
)
|
||||
.expect("second upsert should succeed");
|
||||
|
||||
// Only one record should exist
|
||||
let creds = TuleapCredentials::get(&conn)
|
||||
.expect("get should succeed")
|
||||
.expect("should have credentials");
|
||||
|
||||
assert_eq!(creds.id, second.id);
|
||||
assert_eq!(creds.tuleap_url, "https://new.example.com");
|
||||
assert_eq!(creds.username, "new_user");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_returns_none_when_empty() {
|
||||
let conn = setup();
|
||||
let result = TuleapCredentials::get(&conn).expect("get should succeed");
|
||||
assert!(result.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_returns_credentials() {
|
||||
let conn = setup();
|
||||
let created = TuleapCredentials::upsert(
|
||||
&conn,
|
||||
"https://tuleap.example.com",
|
||||
"bob",
|
||||
"enc_pass",
|
||||
)
|
||||
.expect("upsert should succeed");
|
||||
|
||||
let fetched = TuleapCredentials::get(&conn)
|
||||
.expect("get should succeed")
|
||||
.expect("should have credentials");
|
||||
|
||||
assert_eq!(fetched.id, created.id);
|
||||
assert_eq!(fetched.tuleap_url, "https://tuleap.example.com");
|
||||
assert_eq!(fetched.username, "bob");
|
||||
assert_eq!(fetched.password_encrypted, "enc_pass");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_removes_credentials() {
|
||||
let conn = setup();
|
||||
TuleapCredentials::upsert(&conn, "https://tuleap.example.com", "carol", "enc")
|
||||
.expect("upsert should succeed");
|
||||
|
||||
TuleapCredentials::delete(&conn).expect("delete should succeed");
|
||||
|
||||
let result = TuleapCredentials::get(&conn).expect("get should succeed");
|
||||
assert!(result.is_none());
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +1,2 @@
|
|||
pub mod credential;
|
||||
pub mod project;
|
||||
|
|
|
|||
Loading…
Reference in a new issue