feat: TuleapCredentials model + encrypted storage + Tauri commands

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
thibaud-leclere 2026-04-13 14:26:13 +02:00
parent 9a451061ea
commit 3cf28babab
5 changed files with 251 additions and 0 deletions

View 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())
}

View file

@ -1 +1,2 @@
pub mod credential;
pub mod project;

View file

@ -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");

View 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());
}
}

View file

@ -1 +1,2 @@
pub mod credential;
pub mod project;