parent
f97e075ee6
commit
d75695ffe6
11 changed files with 487 additions and 141 deletions
|
|
@ -0,0 +1,24 @@
|
||||||
|
ALTER TABLE tuleap_credentials RENAME TO tuleap_credentials_legacy;
|
||||||
|
|
||||||
|
CREATE TABLE tuleap_credentials (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
project_id TEXT REFERENCES projects(id) ON DELETE CASCADE,
|
||||||
|
tuleap_url TEXT NOT NULL,
|
||||||
|
username TEXT NOT NULL,
|
||||||
|
password_encrypted TEXT NOT NULL,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO tuleap_credentials (id, project_id, tuleap_url, username, password_encrypted, created_at)
|
||||||
|
SELECT id, NULL, tuleap_url, username, password_encrypted, strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
||||||
|
FROM tuleap_credentials_legacy;
|
||||||
|
|
||||||
|
DROP TABLE tuleap_credentials_legacy;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_tuleap_credentials_project_unique
|
||||||
|
ON tuleap_credentials(project_id)
|
||||||
|
WHERE project_id IS NOT NULL;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_tuleap_credentials_global_unique
|
||||||
|
ON tuleap_credentials((project_id IS NULL))
|
||||||
|
WHERE project_id IS NULL;
|
||||||
|
|
@ -8,6 +8,7 @@ use tauri::State;
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn set_tuleap_credentials(
|
pub fn set_tuleap_credentials(
|
||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
|
project_id: Option<String>,
|
||||||
tuleap_url: String,
|
tuleap_url: String,
|
||||||
username: String,
|
username: String,
|
||||||
password: String,
|
password: String,
|
||||||
|
|
@ -24,44 +25,64 @@ pub fn set_tuleap_credentials(
|
||||||
.lock()
|
.lock()
|
||||||
.map_err(|e| AppError::from(format!("Database lock failed: {}", e)))?;
|
.map_err(|e| AppError::from(format!("Database lock failed: {}", e)))?;
|
||||||
|
|
||||||
let creds = TuleapCredentials::upsert(&db, &tuleap_url, &username, &password_encrypted)?;
|
let creds = TuleapCredentials::upsert_for_scope(
|
||||||
|
&db,
|
||||||
|
project_id.as_deref(),
|
||||||
|
&tuleap_url,
|
||||||
|
&username,
|
||||||
|
&password_encrypted,
|
||||||
|
)?;
|
||||||
Ok(creds.to_safe())
|
Ok(creds.to_safe())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn get_tuleap_credentials(
|
pub fn get_tuleap_credentials(
|
||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
|
project_id: Option<String>,
|
||||||
) -> Result<Option<TuleapCredentialsSafe>, AppError> {
|
) -> Result<Option<TuleapCredentialsSafe>, AppError> {
|
||||||
let db = state
|
let db = state
|
||||||
.db
|
.db
|
||||||
.lock()
|
.lock()
|
||||||
.map_err(|e| AppError::from(format!("Database lock failed: {}", e)))?;
|
.map_err(|e| AppError::from(format!("Database lock failed: {}", e)))?;
|
||||||
|
|
||||||
let result = TuleapCredentials::get(&db)?.map(|c| c.to_safe());
|
let result = match project_id {
|
||||||
|
Some(project_id) => TuleapCredentials::get_for_project(&db, &project_id)?,
|
||||||
|
None => TuleapCredentials::get_global(&db)?,
|
||||||
|
}
|
||||||
|
.map(|c| c.to_safe());
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn delete_tuleap_credentials(state: State<'_, AppState>) -> Result<(), AppError> {
|
pub fn delete_tuleap_credentials(
|
||||||
|
state: State<'_, AppState>,
|
||||||
|
project_id: Option<String>,
|
||||||
|
) -> Result<(), AppError> {
|
||||||
let db = state
|
let db = state
|
||||||
.db
|
.db
|
||||||
.lock()
|
.lock()
|
||||||
.map_err(|e| AppError::from(format!("Database lock failed: {}", e)))?;
|
.map_err(|e| AppError::from(format!("Database lock failed: {}", e)))?;
|
||||||
|
|
||||||
TuleapCredentials::delete(&db)?;
|
TuleapCredentials::delete_for_scope(&db, project_id.as_deref())?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn test_tuleap_connection(state: State<'_, AppState>) -> Result<String, AppError> {
|
pub async fn test_tuleap_connection(
|
||||||
|
state: State<'_, AppState>,
|
||||||
|
project_id: Option<String>,
|
||||||
|
) -> Result<String, AppError> {
|
||||||
let (tuleap_url, username, password) = {
|
let (tuleap_url, username, password) = {
|
||||||
let db = state
|
let db = state
|
||||||
.db
|
.db
|
||||||
.lock()
|
.lock()
|
||||||
.map_err(|e| AppError::from(format!("Database lock failed: {}", e)))?;
|
.map_err(|e| AppError::from(format!("Database lock failed: {}", e)))?;
|
||||||
|
|
||||||
let creds = TuleapCredentials::get(&db)?
|
let creds = match &project_id {
|
||||||
.ok_or_else(|| AppError::from("No credentials configured".to_string()))?;
|
Some(project_id) => TuleapCredentials::get_for_project(&db, project_id)?,
|
||||||
|
None => TuleapCredentials::get_global(&db)?,
|
||||||
|
}
|
||||||
|
.ok_or_else(|| AppError::from("No credentials configured".to_string()))?;
|
||||||
|
|
||||||
let password = crypto::decrypt(&state.encryption_key, &creds.password_encrypted)
|
let password = crypto::decrypt(&state.encryption_key, &creds.password_encrypted)
|
||||||
.map_err(AppError::from)?;
|
.map_err(AppError::from)?;
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ pub async fn manual_poll(
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let cred = TuleapCredentials::get(&db)?
|
let cred = TuleapCredentials::get_for_project(&db, &tracker.project_id)?
|
||||||
.ok_or_else(|| AppError::from("No Tuleap credentials configured".to_string()))?;
|
.ok_or_else(|| AppError::from("No Tuleap credentials configured".to_string()))?;
|
||||||
|
|
||||||
let password = crypto::decrypt(&state.encryption_key, &cred.password_encrypted)
|
let password = crypto::decrypt(&state.encryption_key, &cred.password_encrypted)
|
||||||
|
|
|
||||||
|
|
@ -9,13 +9,16 @@ use crate::AppState;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use tauri::State;
|
use tauri::State;
|
||||||
|
|
||||||
fn build_tuleap_client(state: &State<AppState>) -> Result<TuleapClient, AppError> {
|
fn build_tuleap_client(
|
||||||
|
state: &State<AppState>,
|
||||||
|
project_id: &str,
|
||||||
|
) -> Result<TuleapClient, AppError> {
|
||||||
let db = state
|
let db = state
|
||||||
.db
|
.db
|
||||||
.lock()
|
.lock()
|
||||||
.map_err(|e| AppError::from(format!("Database lock failed: {}", e)))?;
|
.map_err(|e| AppError::from(format!("Database lock failed: {}", e)))?;
|
||||||
|
|
||||||
let cred = TuleapCredentials::get(&db)?
|
let cred = TuleapCredentials::get_for_project(&db, project_id)?
|
||||||
.ok_or_else(|| AppError::from("No Tuleap credentials configured".to_string()))?;
|
.ok_or_else(|| AppError::from("No Tuleap credentials configured".to_string()))?;
|
||||||
|
|
||||||
let password =
|
let password =
|
||||||
|
|
@ -133,9 +136,10 @@ pub fn remove_tracker(state: State<'_, AppState>, id: String) -> Result<(), AppE
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_tracker_fields(
|
pub async fn get_tracker_fields(
|
||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
|
project_id: String,
|
||||||
tracker_id: i32,
|
tracker_id: i32,
|
||||||
) -> Result<Vec<crate::services::tuleap_client::TrackerField>, AppError> {
|
) -> Result<Vec<crate::services::tuleap_client::TrackerField>, AppError> {
|
||||||
let client = build_tuleap_client(&state)?;
|
let client = build_tuleap_client(&state, &project_id)?;
|
||||||
let mut fields = client
|
let mut fields = client
|
||||||
.get_tracker_fields(tracker_id)
|
.get_tracker_fields(tracker_id)
|
||||||
.await
|
.await
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ const MIGRATION_004: &str = include_str!("../migrations/004_default_agents.sql")
|
||||||
const MIGRATION_005: &str = include_str!("../migrations/005_orchestration_modules_chat_tasks.sql");
|
const MIGRATION_005: &str = include_str!("../migrations/005_orchestration_modules_chat_tasks.sql");
|
||||||
const MIGRATION_006: &str = include_str!("../migrations/006_processed_tickets_unique_index.sql");
|
const MIGRATION_006: &str = include_str!("../migrations/006_processed_tickets_unique_index.sql");
|
||||||
const MIGRATION_007: &str = include_str!("../migrations/007_normalize_timestamps_rfc3339.sql");
|
const MIGRATION_007: &str = include_str!("../migrations/007_normalize_timestamps_rfc3339.sql");
|
||||||
|
const MIGRATION_008: &str = include_str!("../migrations/008_project_scoped_tuleap_credentials.sql");
|
||||||
|
|
||||||
pub fn init(db_path: &Path) -> Result<Connection> {
|
pub fn init(db_path: &Path) -> Result<Connection> {
|
||||||
let conn = Connection::open(db_path)?;
|
let conn = Connection::open(db_path)?;
|
||||||
|
|
@ -61,6 +62,10 @@ fn migrate(conn: &Connection) -> Result<()> {
|
||||||
conn.execute_batch(MIGRATION_007)?;
|
conn.execute_batch(MIGRATION_007)?;
|
||||||
conn.pragma_update(None, "user_version", 7)?;
|
conn.pragma_update(None, "user_version", 7)?;
|
||||||
}
|
}
|
||||||
|
if version < 8 {
|
||||||
|
conn.execute_batch(MIGRATION_008)?;
|
||||||
|
conn.pragma_update(None, "user_version", 8)?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -116,7 +121,7 @@ mod tests {
|
||||||
let version: i32 = conn
|
let version: i32 = conn
|
||||||
.pragma_query_value(None, "user_version", |row| row.get(0))
|
.pragma_query_value(None, "user_version", |row| row.get(0))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(version, 7);
|
assert_eq!(version, 8);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
use rusqlite::{params, Connection, Result};
|
use rusqlite::{params, Connection, OptionalExtension, Result};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct TuleapCredentials {
|
pub struct TuleapCredentials {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
|
pub project_id: Option<String>,
|
||||||
pub tuleap_url: String,
|
pub tuleap_url: String,
|
||||||
pub username: String,
|
pub username: String,
|
||||||
pub password_encrypted: String,
|
pub password_encrypted: String,
|
||||||
|
|
@ -13,6 +14,7 @@ pub struct TuleapCredentials {
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct TuleapCredentialsSafe {
|
pub struct TuleapCredentialsSafe {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
|
pub project_id: Option<String>,
|
||||||
pub tuleap_url: String,
|
pub tuleap_url: String,
|
||||||
pub username: String,
|
pub username: String,
|
||||||
}
|
}
|
||||||
|
|
@ -51,55 +53,131 @@ impl TuleapCredentials {
|
||||||
Ok((normalized_url, normalized_username, password.to_string()))
|
Ok((normalized_url, normalized_username, password.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn upsert(
|
pub fn upsert_for_scope(
|
||||||
conn: &Connection,
|
conn: &Connection,
|
||||||
|
project_id: Option<&str>,
|
||||||
tuleap_url: &str,
|
tuleap_url: &str,
|
||||||
username: &str,
|
username: &str,
|
||||||
password_encrypted: &str,
|
password_encrypted: &str,
|
||||||
) -> Result<TuleapCredentials> {
|
) -> Result<TuleapCredentials> {
|
||||||
conn.execute("DELETE FROM tuleap_credentials", [])?;
|
|
||||||
|
|
||||||
let id = Uuid::new_v4().to_string();
|
let id = Uuid::new_v4().to_string();
|
||||||
conn.execute(
|
|
||||||
"INSERT INTO tuleap_credentials (id, tuleap_url, username, password_encrypted) VALUES (?1, ?2, ?3, ?4)",
|
match project_id {
|
||||||
params![id, tuleap_url, username, password_encrypted],
|
Some(project_id) => {
|
||||||
)?;
|
conn.execute(
|
||||||
|
"DELETE FROM tuleap_credentials WHERE project_id = ?1",
|
||||||
|
params![project_id],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO tuleap_credentials (id, project_id, tuleap_url, username, password_encrypted) VALUES (?1, ?2, ?3, ?4, ?5)",
|
||||||
|
params![id, project_id, tuleap_url, username, password_encrypted],
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
conn.execute(
|
||||||
|
"DELETE FROM tuleap_credentials WHERE project_id IS NULL",
|
||||||
|
[],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO tuleap_credentials (id, project_id, tuleap_url, username, password_encrypted) VALUES (?1, NULL, ?2, ?3, ?4)",
|
||||||
|
params![id, tuleap_url, username, password_encrypted],
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(TuleapCredentials {
|
Ok(TuleapCredentials {
|
||||||
id,
|
id,
|
||||||
|
project_id: project_id.map(String::from),
|
||||||
tuleap_url: tuleap_url.to_string(),
|
tuleap_url: tuleap_url.to_string(),
|
||||||
username: username.to_string(),
|
username: username.to_string(),
|
||||||
password_encrypted: password_encrypted.to_string(),
|
password_encrypted: password_encrypted.to_string(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get(conn: &Connection) -> Result<Option<TuleapCredentials>> {
|
pub fn get_by_scope(
|
||||||
let mut stmt = conn.prepare(
|
conn: &Connection,
|
||||||
"SELECT id, tuleap_url, username, password_encrypted FROM tuleap_credentials LIMIT 1",
|
project_id: Option<&str>,
|
||||||
)?;
|
) -> Result<Option<TuleapCredentials>> {
|
||||||
let mut rows = stmt.query_map([], |row| {
|
match project_id {
|
||||||
Ok(TuleapCredentials {
|
Some(project_id) => conn
|
||||||
id: row.get(0)?,
|
.query_row(
|
||||||
tuleap_url: row.get(1)?,
|
"SELECT id, project_id, tuleap_url, username, password_encrypted \
|
||||||
username: row.get(2)?,
|
FROM tuleap_credentials \
|
||||||
password_encrypted: row.get(3)?,
|
WHERE project_id = ?1 \
|
||||||
})
|
LIMIT 1",
|
||||||
})?;
|
params![project_id],
|
||||||
|
|row| {
|
||||||
match rows.next() {
|
Ok(TuleapCredentials {
|
||||||
Some(row) => Ok(Some(row?)),
|
id: row.get(0)?,
|
||||||
None => Ok(None),
|
project_id: row.get(1)?,
|
||||||
|
tuleap_url: row.get(2)?,
|
||||||
|
username: row.get(3)?,
|
||||||
|
password_encrypted: row.get(4)?,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
|
None => conn
|
||||||
|
.query_row(
|
||||||
|
"SELECT id, project_id, tuleap_url, username, password_encrypted \
|
||||||
|
FROM tuleap_credentials \
|
||||||
|
WHERE project_id IS NULL \
|
||||||
|
LIMIT 1",
|
||||||
|
[],
|
||||||
|
|row| {
|
||||||
|
Ok(TuleapCredentials {
|
||||||
|
id: row.get(0)?,
|
||||||
|
project_id: row.get(1)?,
|
||||||
|
tuleap_url: row.get(2)?,
|
||||||
|
username: row.get(3)?,
|
||||||
|
password_encrypted: row.get(4)?,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete(conn: &Connection) -> Result<()> {
|
pub fn get_global(conn: &Connection) -> Result<Option<TuleapCredentials>> {
|
||||||
conn.execute("DELETE FROM tuleap_credentials", [])?;
|
Self::get_by_scope(conn, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_for_project(
|
||||||
|
conn: &Connection,
|
||||||
|
project_id: &str,
|
||||||
|
) -> Result<Option<TuleapCredentials>> {
|
||||||
|
if let Some(project_scoped) = Self::get_by_scope(conn, Some(project_id))? {
|
||||||
|
return Ok(Some(project_scoped));
|
||||||
|
}
|
||||||
|
|
||||||
|
Self::get_global(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete_for_scope(conn: &Connection, project_id: Option<&str>) -> Result<()> {
|
||||||
|
match project_id {
|
||||||
|
Some(project_id) => {
|
||||||
|
conn.execute(
|
||||||
|
"DELETE FROM tuleap_credentials WHERE project_id = ?1",
|
||||||
|
params![project_id],
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
conn.execute(
|
||||||
|
"DELETE FROM tuleap_credentials WHERE project_id IS NULL",
|
||||||
|
[],
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_safe(&self) -> TuleapCredentialsSafe {
|
pub fn to_safe(&self) -> TuleapCredentialsSafe {
|
||||||
TuleapCredentialsSafe {
|
TuleapCredentialsSafe {
|
||||||
id: self.id.clone(),
|
id: self.id.clone(),
|
||||||
|
project_id: self.project_id.clone(),
|
||||||
tuleap_url: self.tuleap_url.clone(),
|
tuleap_url: self.tuleap_url.clone(),
|
||||||
username: self.username.clone(),
|
username: self.username.clone(),
|
||||||
}
|
}
|
||||||
|
|
@ -110,22 +188,30 @@ impl TuleapCredentials {
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::db;
|
use crate::db;
|
||||||
|
use crate::models::project::Project;
|
||||||
|
|
||||||
fn setup() -> Connection {
|
fn setup() -> Connection {
|
||||||
db::init_in_memory().expect("db init should succeed")
|
db::init_in_memory().expect("db init should succeed")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn create_project(conn: &Connection, name: &str) -> Project {
|
||||||
|
Project::insert(conn, name, &format!("/tmp/{}", name), None, "main")
|
||||||
|
.expect("project insert should succeed")
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_upsert_creates_credentials() {
|
fn test_upsert_global_creates_credentials() {
|
||||||
let conn = setup();
|
let conn = setup();
|
||||||
let creds = TuleapCredentials::upsert(
|
let creds = TuleapCredentials::upsert_for_scope(
|
||||||
&conn,
|
&conn,
|
||||||
|
None,
|
||||||
"https://tuleap.example.com",
|
"https://tuleap.example.com",
|
||||||
"alice",
|
"alice",
|
||||||
"encrypted_password",
|
"encrypted_password",
|
||||||
)
|
)
|
||||||
.expect("upsert should succeed");
|
.expect("upsert should succeed");
|
||||||
|
|
||||||
|
assert_eq!(creds.project_id, None);
|
||||||
assert_eq!(creds.tuleap_url, "https://tuleap.example.com");
|
assert_eq!(creds.tuleap_url, "https://tuleap.example.com");
|
||||||
assert_eq!(creds.username, "alice");
|
assert_eq!(creds.username, "alice");
|
||||||
assert_eq!(creds.password_encrypted, "encrypted_password");
|
assert_eq!(creds.password_encrypted, "encrypted_password");
|
||||||
|
|
@ -133,59 +219,136 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_upsert_replaces_existing() {
|
fn test_upsert_for_scope_replaces_only_target_scope() {
|
||||||
let conn = setup();
|
let conn = setup();
|
||||||
TuleapCredentials::upsert(&conn, "https://old.example.com", "old_user", "old_enc")
|
let project_a = create_project(&conn, "project-a");
|
||||||
.expect("first upsert should succeed");
|
let project_b = create_project(&conn, "project-b");
|
||||||
|
|
||||||
let second =
|
TuleapCredentials::upsert_for_scope(
|
||||||
TuleapCredentials::upsert(&conn, "https://new.example.com", "new_user", "new_enc")
|
&conn,
|
||||||
.expect("second upsert should succeed");
|
Some(&project_a.id),
|
||||||
|
"https://a.example.com",
|
||||||
|
"alice",
|
||||||
|
"a1",
|
||||||
|
)
|
||||||
|
.expect("upsert for project A should succeed");
|
||||||
|
|
||||||
// Only one record should exist
|
TuleapCredentials::upsert_for_scope(
|
||||||
let creds = TuleapCredentials::get(&conn)
|
&conn,
|
||||||
.expect("get should succeed")
|
Some(&project_b.id),
|
||||||
.expect("should have credentials");
|
"https://b.example.com",
|
||||||
|
"bob",
|
||||||
|
"b1",
|
||||||
|
)
|
||||||
|
.expect("upsert for project B should succeed");
|
||||||
|
|
||||||
assert_eq!(creds.id, second.id);
|
TuleapCredentials::upsert_for_scope(
|
||||||
assert_eq!(creds.tuleap_url, "https://new.example.com");
|
&conn,
|
||||||
assert_eq!(creds.username, "new_user");
|
Some(&project_a.id),
|
||||||
|
"https://a2.example.com",
|
||||||
|
"alice2",
|
||||||
|
"a2",
|
||||||
|
)
|
||||||
|
.expect("second upsert for project A should succeed");
|
||||||
|
|
||||||
|
let creds_a = TuleapCredentials::get_by_scope(&conn, Some(&project_a.id))
|
||||||
|
.expect("get project A should succeed")
|
||||||
|
.expect("project A should have credentials");
|
||||||
|
let creds_b = TuleapCredentials::get_by_scope(&conn, Some(&project_b.id))
|
||||||
|
.expect("get project B should succeed")
|
||||||
|
.expect("project B should have credentials");
|
||||||
|
|
||||||
|
assert_eq!(creds_a.tuleap_url, "https://a2.example.com");
|
||||||
|
assert_eq!(creds_a.username, "alice2");
|
||||||
|
assert_eq!(creds_b.tuleap_url, "https://b.example.com");
|
||||||
|
assert_eq!(creds_b.username, "bob");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_get_returns_none_when_empty() {
|
fn test_get_for_project_falls_back_to_global() {
|
||||||
let conn = setup();
|
let conn = setup();
|
||||||
let result = TuleapCredentials::get(&conn).expect("get should succeed");
|
let project = create_project(&conn, "project-with-fallback");
|
||||||
assert!(result.is_none());
|
|
||||||
|
TuleapCredentials::upsert_for_scope(
|
||||||
|
&conn,
|
||||||
|
None,
|
||||||
|
"https://global.example.com",
|
||||||
|
"global",
|
||||||
|
"enc",
|
||||||
|
)
|
||||||
|
.expect("global upsert should succeed");
|
||||||
|
|
||||||
|
let creds = TuleapCredentials::get_for_project(&conn, &project.id)
|
||||||
|
.expect("get for project should succeed")
|
||||||
|
.expect("fallback global credentials should be returned");
|
||||||
|
|
||||||
|
assert_eq!(creds.project_id, None);
|
||||||
|
assert_eq!(creds.tuleap_url, "https://global.example.com");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_get_returns_credentials() {
|
fn test_get_for_project_prefers_scoped_credentials() {
|
||||||
let conn = setup();
|
let conn = setup();
|
||||||
let created =
|
let project = create_project(&conn, "project-scoped");
|
||||||
TuleapCredentials::upsert(&conn, "https://tuleap.example.com", "bob", "enc_pass")
|
|
||||||
.expect("upsert should succeed");
|
|
||||||
|
|
||||||
let fetched = TuleapCredentials::get(&conn)
|
TuleapCredentials::upsert_for_scope(
|
||||||
.expect("get should succeed")
|
&conn,
|
||||||
.expect("should have credentials");
|
None,
|
||||||
|
"https://global.example.com",
|
||||||
|
"global",
|
||||||
|
"global",
|
||||||
|
)
|
||||||
|
.expect("global upsert should succeed");
|
||||||
|
|
||||||
assert_eq!(fetched.id, created.id);
|
TuleapCredentials::upsert_for_scope(
|
||||||
assert_eq!(fetched.tuleap_url, "https://tuleap.example.com");
|
&conn,
|
||||||
assert_eq!(fetched.username, "bob");
|
Some(&project.id),
|
||||||
assert_eq!(fetched.password_encrypted, "enc_pass");
|
"https://project.example.com",
|
||||||
|
"project-user",
|
||||||
|
"project-enc",
|
||||||
|
)
|
||||||
|
.expect("project scoped upsert should succeed");
|
||||||
|
|
||||||
|
let creds = TuleapCredentials::get_for_project(&conn, &project.id)
|
||||||
|
.expect("get for project should succeed")
|
||||||
|
.expect("project credentials should exist");
|
||||||
|
|
||||||
|
assert_eq!(creds.project_id, Some(project.id));
|
||||||
|
assert_eq!(creds.tuleap_url, "https://project.example.com");
|
||||||
|
assert_eq!(creds.username, "project-user");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_delete_removes_credentials() {
|
fn test_delete_for_scope_removes_only_target_scope() {
|
||||||
let conn = setup();
|
let conn = setup();
|
||||||
TuleapCredentials::upsert(&conn, "https://tuleap.example.com", "carol", "enc")
|
let project = create_project(&conn, "project-delete");
|
||||||
.expect("upsert should succeed");
|
|
||||||
|
|
||||||
TuleapCredentials::delete(&conn).expect("delete should succeed");
|
TuleapCredentials::upsert_for_scope(
|
||||||
|
&conn,
|
||||||
|
None,
|
||||||
|
"https://global.example.com",
|
||||||
|
"global",
|
||||||
|
"enc",
|
||||||
|
)
|
||||||
|
.expect("global upsert should succeed");
|
||||||
|
TuleapCredentials::upsert_for_scope(
|
||||||
|
&conn,
|
||||||
|
Some(&project.id),
|
||||||
|
"https://project.example.com",
|
||||||
|
"project-user",
|
||||||
|
"project-enc",
|
||||||
|
)
|
||||||
|
.expect("project scoped upsert should succeed");
|
||||||
|
|
||||||
let result = TuleapCredentials::get(&conn).expect("get should succeed");
|
TuleapCredentials::delete_for_scope(&conn, Some(&project.id))
|
||||||
assert!(result.is_none());
|
.expect("delete for project scope should succeed");
|
||||||
|
|
||||||
|
let scoped = TuleapCredentials::get_by_scope(&conn, Some(&project.id))
|
||||||
|
.expect("scoped fetch should succeed");
|
||||||
|
let global = TuleapCredentials::get_global(&conn).expect("global fetch should succeed");
|
||||||
|
|
||||||
|
assert!(scoped.is_none());
|
||||||
|
assert!(global.is_some());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -29,8 +29,8 @@ async fn poll_all_trackers(
|
||||||
http_client: &reqwest::Client,
|
http_client: &reqwest::Client,
|
||||||
app_handle: &AppHandle,
|
app_handle: &AppHandle,
|
||||||
) {
|
) {
|
||||||
// 1. Read all enabled trackers and credentials from DB
|
// 1. Read all enabled trackers from DB
|
||||||
let (trackers, client) = {
|
let trackers = {
|
||||||
let conn = match db.lock() {
|
let conn = match db.lock() {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|
@ -47,34 +47,57 @@ async fn poll_all_trackers(
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 2. Read credentials; bail silently if none
|
trackers
|
||||||
let creds = match TuleapCredentials::get(&conn) {
|
|
||||||
Ok(Some(c)) => c,
|
|
||||||
Ok(None) => return,
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("poller: failed to read credentials: {}", e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let password = match crypto::decrypt(encryption_key, &creds.password_encrypted) {
|
|
||||||
Ok(p) => p,
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("poller: failed to decrypt password: {}", e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let client = TuleapClient::new(http_client, &creds.tuleap_url, &creds.username, &password);
|
|
||||||
|
|
||||||
(trackers, client)
|
|
||||||
}; // lock released
|
}; // lock released
|
||||||
|
|
||||||
// 3. For each tracker that should_poll, poll it
|
// 2. For each tracker that should_poll, poll it
|
||||||
for tracker in &trackers {
|
for tracker in &trackers {
|
||||||
if should_poll(tracker) {
|
if !should_poll(tracker) {
|
||||||
poll_single_tracker(db, &client, tracker, app_handle).await;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let client = {
|
||||||
|
let conn = match db.lock() {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("poller: failed to lock db while preparing client: {}", e);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let creds = match TuleapCredentials::get_for_project(&conn, &tracker.project_id) {
|
||||||
|
Ok(Some(c)) => c,
|
||||||
|
Ok(None) => {
|
||||||
|
eprintln!(
|
||||||
|
"poller: no credentials configured for project {} (tracker {})",
|
||||||
|
tracker.project_id, tracker.id
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!(
|
||||||
|
"poller: failed to read credentials for project {}: {}",
|
||||||
|
tracker.project_id, e
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let password = match crypto::decrypt(encryption_key, &creds.password_encrypted) {
|
||||||
|
Ok(p) => p,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!(
|
||||||
|
"poller: failed to decrypt password for project {}: {}",
|
||||||
|
tracker.project_id, e
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
TuleapClient::new(http_client, &creds.tuleap_url, &creds.username, &password)
|
||||||
|
};
|
||||||
|
|
||||||
|
poll_single_tracker(db, &client, tracker, app_handle).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,103 @@
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
|
import { useSearchParams } from "react-router-dom";
|
||||||
import {
|
import {
|
||||||
|
listProjects,
|
||||||
getTuleapCredentials,
|
getTuleapCredentials,
|
||||||
setTuleapCredentials,
|
setTuleapCredentials,
|
||||||
deleteTuleapCredentials,
|
deleteTuleapCredentials,
|
||||||
testTuleapConnection,
|
testTuleapConnection,
|
||||||
} from "../../lib/api";
|
} from "../../lib/api";
|
||||||
import { getErrorMessage } from "../../lib/errors";
|
import { getErrorMessage } from "../../lib/errors";
|
||||||
import type { TuleapCredentialsSafe } from "../../lib/types";
|
import type { Project, TuleapCredentialsSafe } from "../../lib/types";
|
||||||
import ConfirmModal from "../ui/ConfirmModal";
|
import ConfirmModal from "../ui/ConfirmModal";
|
||||||
|
|
||||||
|
function normalizeScope(value: string): string | null {
|
||||||
|
return value === "" ? null : value;
|
||||||
|
}
|
||||||
|
|
||||||
export default function SettingsPage() {
|
export default function SettingsPage() {
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const initialProjectId = searchParams.get("projectId");
|
||||||
|
|
||||||
|
const [projects, setProjects] = useState<Project[]>([]);
|
||||||
|
const [selectedProjectId, setSelectedProjectId] = useState<string | null>(initialProjectId);
|
||||||
|
|
||||||
const [tuleapUrl, setTuleapUrl] = useState("");
|
const [tuleapUrl, setTuleapUrl] = useState("");
|
||||||
const [username, setUsername] = useState("");
|
const [username, setUsername] = useState("");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [existing, setExisting] = useState<TuleapCredentialsSafe | null>(null);
|
const [existing, setExisting] = useState<TuleapCredentialsSafe | null>(null);
|
||||||
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [success, setSuccess] = useState<string | null>(null);
|
const [success, setSuccess] = useState<string | null>(null);
|
||||||
|
const [loadingScope, setLoadingScope] = useState(false);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [testing, setTesting] = useState(false);
|
const [testing, setTesting] = useState(false);
|
||||||
const [deleting, setDeleting] = useState(false);
|
const [deleting, setDeleting] = useState(false);
|
||||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||||
|
|
||||||
|
const selectedProject =
|
||||||
|
selectedProjectId === null
|
||||||
|
? null
|
||||||
|
: projects.find((project) => project.id === selectedProjectId) ?? null;
|
||||||
|
|
||||||
|
const hasScopedCredentials =
|
||||||
|
existing !== null && (existing.project_id ?? null) === selectedProjectId;
|
||||||
|
|
||||||
|
const usingGlobalFallback =
|
||||||
|
selectedProjectId !== null && existing !== null && existing.project_id !== selectedProjectId;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getTuleapCredentials().then((creds) => {
|
listProjects()
|
||||||
if (creds) {
|
.then((items) => {
|
||||||
|
setProjects(items);
|
||||||
|
if (initialProjectId && !items.some((project) => project.id === initialProjectId)) {
|
||||||
|
setSelectedProjectId(null);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err: unknown) => {
|
||||||
|
setError(getErrorMessage(err));
|
||||||
|
});
|
||||||
|
}, [initialProjectId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
|
setLoadingScope(true);
|
||||||
|
setError(null);
|
||||||
|
setSuccess(null);
|
||||||
|
|
||||||
|
getTuleapCredentials(selectedProjectId)
|
||||||
|
.then((creds) => {
|
||||||
|
if (cancelled) return;
|
||||||
|
|
||||||
setExisting(creds);
|
setExisting(creds);
|
||||||
setTuleapUrl(creds.tuleap_url);
|
if (creds) {
|
||||||
setUsername(creds.username);
|
setTuleapUrl(creds.tuleap_url);
|
||||||
}
|
setUsername(creds.username);
|
||||||
});
|
} else {
|
||||||
}, []);
|
setTuleapUrl("");
|
||||||
|
setUsername("");
|
||||||
|
}
|
||||||
|
setPassword("");
|
||||||
|
})
|
||||||
|
.catch((err: unknown) => {
|
||||||
|
if (cancelled) return;
|
||||||
|
setExisting(null);
|
||||||
|
setTuleapUrl("");
|
||||||
|
setUsername("");
|
||||||
|
setPassword("");
|
||||||
|
setError(getErrorMessage(err));
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (!cancelled) {
|
||||||
|
setLoadingScope(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [selectedProjectId]);
|
||||||
|
|
||||||
function clearMessages() {
|
function clearMessages() {
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
@ -41,10 +109,10 @@ export default function SettingsPage() {
|
||||||
clearMessages();
|
clearMessages();
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
try {
|
try {
|
||||||
const creds = await setTuleapCredentials(tuleapUrl, username, password);
|
const creds = await setTuleapCredentials(selectedProjectId, tuleapUrl, username, password);
|
||||||
setExisting(creds);
|
setExisting(creds);
|
||||||
setPassword("");
|
setPassword("");
|
||||||
setSuccess("Credentials saved.");
|
setSuccess(selectedProjectId ? "Project credentials saved." : "Global credentials saved.");
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
setError(getErrorMessage(err));
|
setError(getErrorMessage(err));
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -56,7 +124,7 @@ export default function SettingsPage() {
|
||||||
clearMessages();
|
clearMessages();
|
||||||
setTesting(true);
|
setTesting(true);
|
||||||
try {
|
try {
|
||||||
const msg = await testTuleapConnection();
|
const msg = await testTuleapConnection(selectedProjectId);
|
||||||
setSuccess(msg);
|
setSuccess(msg);
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
setError(getErrorMessage(err));
|
setError(getErrorMessage(err));
|
||||||
|
|
@ -70,12 +138,19 @@ export default function SettingsPage() {
|
||||||
clearMessages();
|
clearMessages();
|
||||||
setDeleting(true);
|
setDeleting(true);
|
||||||
try {
|
try {
|
||||||
await deleteTuleapCredentials();
|
await deleteTuleapCredentials(selectedProjectId);
|
||||||
setExisting(null);
|
|
||||||
setTuleapUrl("");
|
const refreshed = await getTuleapCredentials(selectedProjectId);
|
||||||
setUsername("");
|
setExisting(refreshed);
|
||||||
|
setTuleapUrl(refreshed?.tuleap_url ?? "");
|
||||||
|
setUsername(refreshed?.username ?? "");
|
||||||
setPassword("");
|
setPassword("");
|
||||||
setSuccess("Credentials deleted.");
|
|
||||||
|
if (selectedProjectId && refreshed && refreshed.project_id === null) {
|
||||||
|
setSuccess("Project credentials deleted. Global fallback credentials are now used.");
|
||||||
|
} else {
|
||||||
|
setSuccess("Credentials deleted.");
|
||||||
|
}
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
setError(getErrorMessage(err));
|
setError(getErrorMessage(err));
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -90,11 +165,40 @@ export default function SettingsPage() {
|
||||||
<div className="bg-white rounded-lg border border-gray-200 p-6">
|
<div className="bg-white rounded-lg border border-gray-200 p-6">
|
||||||
<h3 className="text-base font-semibold mb-4">Tuleap credentials</h3>
|
<h3 className="text-base font-semibold mb-4">Tuleap credentials</h3>
|
||||||
|
|
||||||
|
<div className="mb-4">
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">Scope</label>
|
||||||
|
<select
|
||||||
|
value={selectedProjectId ?? ""}
|
||||||
|
onChange={(e) => setSelectedProjectId(normalizeScope(e.target.value))}
|
||||||
|
className="w-full border border-gray-300 rounded px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
>
|
||||||
|
<option value="">Global fallback</option>
|
||||||
|
{projects.map((project) => (
|
||||||
|
<option key={project.id} value={project.id}>
|
||||||
|
{project.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<p className="mt-2 text-xs text-gray-500">
|
||||||
|
{selectedProject
|
||||||
|
? `Les trackers du projet \"${selectedProject.name}\" utilisent d'abord ce scope, puis le fallback global.`
|
||||||
|
: "Credentials par défaut pour tous les projets sans credentials dédiés."}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{loadingScope ? (
|
||||||
|
<div className="text-sm text-gray-500 mb-4">Loading credentials...</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{usingGlobalFallback && (
|
||||||
|
<div className="mb-4 rounded border border-amber-200 bg-amber-50 p-3 text-sm text-amber-700">
|
||||||
|
Aucun credential spécifique au projet n'est configuré. Le fallback global est utilisé.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<form onSubmit={handleSave} className="space-y-4">
|
<form onSubmit={handleSave} className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">Tuleap URL</label>
|
||||||
Tuleap URL
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
type="url"
|
type="url"
|
||||||
value={tuleapUrl}
|
value={tuleapUrl}
|
||||||
|
|
@ -106,9 +210,7 @@ export default function SettingsPage() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">Username</label>
|
||||||
Username
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={username}
|
value={username}
|
||||||
|
|
@ -119,14 +221,12 @@ export default function SettingsPage() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">Password</label>
|
||||||
Password
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
placeholder={existing ? "Leave empty to keep current" : ""}
|
required
|
||||||
className="w-full border border-gray-300 rounded px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
className="w-full border border-gray-300 rounded px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -146,7 +246,7 @@ export default function SettingsPage() {
|
||||||
<div className="flex gap-2 flex-wrap">
|
<div className="flex gap-2 flex-wrap">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={saving}
|
disabled={saving || loadingScope}
|
||||||
className="px-4 py-2 bg-blue-600 text-white rounded text-sm hover:bg-blue-700 disabled:opacity-50"
|
className="px-4 py-2 bg-blue-600 text-white rounded text-sm hover:bg-blue-700 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{saving ? "Saving..." : "Save"}
|
{saving ? "Saving..." : "Save"}
|
||||||
|
|
@ -154,12 +254,12 @@ export default function SettingsPage() {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleTest}
|
onClick={handleTest}
|
||||||
disabled={testing || !existing}
|
disabled={testing || !existing || loadingScope}
|
||||||
className="px-4 py-2 bg-gray-200 rounded text-sm hover:bg-gray-300 disabled:opacity-50"
|
className="px-4 py-2 bg-gray-200 rounded text-sm hover:bg-gray-300 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{testing ? "Testing..." : "Test connection"}
|
{testing ? "Testing..." : "Test connection"}
|
||||||
</button>
|
</button>
|
||||||
{existing && (
|
{hasScopedCredentials && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setIsDeleteModalOpen(true)}
|
onClick={() => setIsDeleteModalOpen(true)}
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,7 @@ export default function TrackerConfig() {
|
||||||
setEnabled(tracker.enabled);
|
setEnabled(tracker.enabled);
|
||||||
setTrackerStatus(tracker.status === "invalid" ? "invalid" : "valid");
|
setTrackerStatus(tracker.status === "invalid" ? "invalid" : "valid");
|
||||||
|
|
||||||
const trackerFields = await getTrackerFields(tracker.tracker_id);
|
const trackerFields = await getTrackerFields(projectId, tracker.tracker_id);
|
||||||
setFields(sortTrackerFields(trackerFields));
|
setFields(sortTrackerFields(trackerFields));
|
||||||
setFieldsLoaded(true);
|
setFieldsLoaded(true);
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
|
|
@ -107,11 +107,11 @@ export default function TrackerConfig() {
|
||||||
}, [projectId, trackerConfigId]);
|
}, [projectId, trackerConfigId]);
|
||||||
|
|
||||||
async function handleLoadFields() {
|
async function handleLoadFields() {
|
||||||
if (!trackerId) return;
|
if (!trackerId || !projectId) return;
|
||||||
setFieldsLoading(true);
|
setFieldsLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const result = await getTrackerFields(Number(trackerId));
|
const result = await getTrackerFields(projectId, Number(trackerId));
|
||||||
setFields(sortTrackerFields(result));
|
setFields(sortTrackerFields(result));
|
||||||
setFieldsLoaded(true);
|
setFieldsLoaded(true);
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
|
|
|
||||||
|
|
@ -89,17 +89,22 @@ export async function deleteAgent(id: string): Promise<void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Credentials
|
// Credentials
|
||||||
export async function setTuleapCredentials(tuleapUrl: string, username: string, password: string): Promise<TuleapCredentialsSafe> {
|
export async function setTuleapCredentials(
|
||||||
return invoke("set_tuleap_credentials", { tuleapUrl, username, password });
|
projectId: string | null,
|
||||||
|
tuleapUrl: string,
|
||||||
|
username: string,
|
||||||
|
password: string
|
||||||
|
): Promise<TuleapCredentialsSafe> {
|
||||||
|
return invoke("set_tuleap_credentials", { projectId, tuleapUrl, username, password });
|
||||||
}
|
}
|
||||||
export async function getTuleapCredentials(): Promise<TuleapCredentialsSafe | null> {
|
export async function getTuleapCredentials(projectId: string | null): Promise<TuleapCredentialsSafe | null> {
|
||||||
return invoke("get_tuleap_credentials");
|
return invoke("get_tuleap_credentials", { projectId });
|
||||||
}
|
}
|
||||||
export async function deleteTuleapCredentials(): Promise<void> {
|
export async function deleteTuleapCredentials(projectId: string | null): Promise<void> {
|
||||||
return invoke("delete_tuleap_credentials");
|
return invoke("delete_tuleap_credentials", { projectId });
|
||||||
}
|
}
|
||||||
export async function testTuleapConnection(): Promise<string> {
|
export async function testTuleapConnection(projectId: string | null): Promise<string> {
|
||||||
return invoke("test_tuleap_connection");
|
return invoke("test_tuleap_connection", { projectId });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trackers
|
// Trackers
|
||||||
|
|
@ -153,8 +158,8 @@ export async function updateTracker(
|
||||||
export async function removeTracker(id: string): Promise<void> {
|
export async function removeTracker(id: string): Promise<void> {
|
||||||
return invoke("remove_tracker", { id });
|
return invoke("remove_tracker", { id });
|
||||||
}
|
}
|
||||||
export async function getTrackerFields(trackerId: number): Promise<TrackerField[]> {
|
export async function getTrackerFields(projectId: string, trackerId: number): Promise<TrackerField[]> {
|
||||||
return invoke("get_tracker_fields", { trackerId });
|
return invoke("get_tracker_fields", { projectId, trackerId });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tickets & Polling
|
// Tickets & Polling
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ export interface Project {
|
||||||
|
|
||||||
export interface TuleapCredentialsSafe {
|
export interface TuleapCredentialsSafe {
|
||||||
id: string;
|
id: string;
|
||||||
|
project_id: string | null;
|
||||||
tuleap_url: string;
|
tuleap_url: string;
|
||||||
username: string;
|
username: string;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue