tftsr-devops_investigation/src-tauri/src/integrations/webview_auth.rs

271 lines
9.3 KiB
Rust
Raw Normal View History

use serde::{Deserialize, Serialize};
fix: persist integration settings and implement persistent browser windows ## Integration Settings Persistence - Add database commands to save/load integration configs (base_url, username, project_name, space_key) - Frontend now loads configs from DB on mount and saves changes automatically - Fixes issue where settings were lost on app restart ## Persistent Browser Window Architecture - Integration browser windows now stay open for user browsing and authentication - Extract fresh cookies before each API call to handle token rotation - Track open windows in app state (integration_webviews HashMap) - Windows titled as "{Service} Browser (TFTSR)" for clarity - Support easy navigation between app and browser windows (Cmd+Tab/Alt+Tab) - Gracefully handle closed windows with automatic cleanup ## Bug Fixes - Fix Rust formatting issues across 8 files - Fix clippy warnings: - Use is_some_and() instead of map_or() in openai.rs - Use .to_string() instead of format!() in integrations.rs - Add missing OptionalExtension import for .optional() method ## Tests - Add test_integration_config_serialization - Add test_webview_tracking - Add test_token_auth_request_serialization - All 6 integration tests passing ## Files Modified - src-tauri/src/state.rs: Add integration_webviews tracking - src-tauri/src/lib.rs: Register 3 new commands, initialize webviews HashMap - src-tauri/src/commands/integrations.rs: Config persistence, fresh cookie extraction (+151 lines) - src-tauri/src/integrations/webview_auth.rs: Persistent window behavior - src/lib/tauriCommands.ts: TypeScript wrappers for new commands - src/pages/Settings/Integrations.tsx: Load/save configs from DB Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-04-04 14:57:22 +00:00
use tauri::{AppHandle, Listener, WebviewUrl, WebviewWindow, WebviewWindowBuilder};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExtractedCredentials {
pub cookies: Vec<Cookie>,
pub service: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Cookie {
pub name: String,
pub value: String,
pub domain: String,
pub path: String,
pub secure: bool,
pub http_only: bool,
pub expires: Option<i64>,
}
/// Open an embedded browser window for the user to log in and extract cookies.
/// This approach works when user is off-VPN (can access web UI) but APIs require VPN.
pub async fn authenticate_with_webview(
app_handle: AppHandle,
service: &str,
base_url: &str,
) -> Result<ExtractedCredentials, String> {
let trimmed_base_url = base_url.trim_end_matches('/');
let login_url = match service {
"confluence" => format!("{trimmed_base_url}/login.action"),
"azuredevops" => {
// Azure DevOps login - user will be redirected through Microsoft SSO
format!("{trimmed_base_url}/_signin")
}
"servicenow" => format!("{trimmed_base_url}/login.do"),
_ => return Err(format!("Unknown service: {service}")),
};
fix: persist integration settings and implement persistent browser windows ## Integration Settings Persistence - Add database commands to save/load integration configs (base_url, username, project_name, space_key) - Frontend now loads configs from DB on mount and saves changes automatically - Fixes issue where settings were lost on app restart ## Persistent Browser Window Architecture - Integration browser windows now stay open for user browsing and authentication - Extract fresh cookies before each API call to handle token rotation - Track open windows in app state (integration_webviews HashMap) - Windows titled as "{Service} Browser (TFTSR)" for clarity - Support easy navigation between app and browser windows (Cmd+Tab/Alt+Tab) - Gracefully handle closed windows with automatic cleanup ## Bug Fixes - Fix Rust formatting issues across 8 files - Fix clippy warnings: - Use is_some_and() instead of map_or() in openai.rs - Use .to_string() instead of format!() in integrations.rs - Add missing OptionalExtension import for .optional() method ## Tests - Add test_integration_config_serialization - Add test_webview_tracking - Add test_token_auth_request_serialization - All 6 integration tests passing ## Files Modified - src-tauri/src/state.rs: Add integration_webviews tracking - src-tauri/src/lib.rs: Register 3 new commands, initialize webviews HashMap - src-tauri/src/commands/integrations.rs: Config persistence, fresh cookie extraction (+151 lines) - src-tauri/src/integrations/webview_auth.rs: Persistent window behavior - src/lib/tauriCommands.ts: TypeScript wrappers for new commands - src/pages/Settings/Integrations.tsx: Load/save configs from DB Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-04-04 14:57:22 +00:00
tracing::info!(
"Opening persistent browser for {} at {}",
service,
login_url
);
fix: persist integration settings and implement persistent browser windows ## Integration Settings Persistence - Add database commands to save/load integration configs (base_url, username, project_name, space_key) - Frontend now loads configs from DB on mount and saves changes automatically - Fixes issue where settings were lost on app restart ## Persistent Browser Window Architecture - Integration browser windows now stay open for user browsing and authentication - Extract fresh cookies before each API call to handle token rotation - Track open windows in app state (integration_webviews HashMap) - Windows titled as "{Service} Browser (TFTSR)" for clarity - Support easy navigation between app and browser windows (Cmd+Tab/Alt+Tab) - Gracefully handle closed windows with automatic cleanup ## Bug Fixes - Fix Rust formatting issues across 8 files - Fix clippy warnings: - Use is_some_and() instead of map_or() in openai.rs - Use .to_string() instead of format!() in integrations.rs - Add missing OptionalExtension import for .optional() method ## Tests - Add test_integration_config_serialization - Add test_webview_tracking - Add test_token_auth_request_serialization - All 6 integration tests passing ## Files Modified - src-tauri/src/state.rs: Add integration_webviews tracking - src-tauri/src/lib.rs: Register 3 new commands, initialize webviews HashMap - src-tauri/src/commands/integrations.rs: Config persistence, fresh cookie extraction (+151 lines) - src-tauri/src/integrations/webview_auth.rs: Persistent window behavior - src/lib/tauriCommands.ts: TypeScript wrappers for new commands - src/pages/Settings/Integrations.tsx: Load/save configs from DB Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-04-04 14:57:22 +00:00
// Create persistent browser window (stays open for browsing and fresh cookie extraction)
let webview_label = format!("{service}-auth");
fix: persist integration settings and implement persistent browser windows ## Integration Settings Persistence - Add database commands to save/load integration configs (base_url, username, project_name, space_key) - Frontend now loads configs from DB on mount and saves changes automatically - Fixes issue where settings were lost on app restart ## Persistent Browser Window Architecture - Integration browser windows now stay open for user browsing and authentication - Extract fresh cookies before each API call to handle token rotation - Track open windows in app state (integration_webviews HashMap) - Windows titled as "{Service} Browser (TFTSR)" for clarity - Support easy navigation between app and browser windows (Cmd+Tab/Alt+Tab) - Gracefully handle closed windows with automatic cleanup ## Bug Fixes - Fix Rust formatting issues across 8 files - Fix clippy warnings: - Use is_some_and() instead of map_or() in openai.rs - Use .to_string() instead of format!() in integrations.rs - Add missing OptionalExtension import for .optional() method ## Tests - Add test_integration_config_serialization - Add test_webview_tracking - Add test_token_auth_request_serialization - All 6 integration tests passing ## Files Modified - src-tauri/src/state.rs: Add integration_webviews tracking - src-tauri/src/lib.rs: Register 3 new commands, initialize webviews HashMap - src-tauri/src/commands/integrations.rs: Config persistence, fresh cookie extraction (+151 lines) - src-tauri/src/integrations/webview_auth.rs: Persistent window behavior - src/lib/tauriCommands.ts: TypeScript wrappers for new commands - src/pages/Settings/Integrations.tsx: Load/save configs from DB Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-04-04 14:57:22 +00:00
let webview = WebviewWindowBuilder::new(
&app_handle,
&webview_label,
WebviewUrl::External(login_url.parse().map_err(|e| format!("Invalid URL: {e}"))?),
)
.title(format!(
"{service} Browser (Troubleshooting and RCA Assistant)"
))
fix: persist integration settings and implement persistent browser windows ## Integration Settings Persistence - Add database commands to save/load integration configs (base_url, username, project_name, space_key) - Frontend now loads configs from DB on mount and saves changes automatically - Fixes issue where settings were lost on app restart ## Persistent Browser Window Architecture - Integration browser windows now stay open for user browsing and authentication - Extract fresh cookies before each API call to handle token rotation - Track open windows in app state (integration_webviews HashMap) - Windows titled as "{Service} Browser (TFTSR)" for clarity - Support easy navigation between app and browser windows (Cmd+Tab/Alt+Tab) - Gracefully handle closed windows with automatic cleanup ## Bug Fixes - Fix Rust formatting issues across 8 files - Fix clippy warnings: - Use is_some_and() instead of map_or() in openai.rs - Use .to_string() instead of format!() in integrations.rs - Add missing OptionalExtension import for .optional() method ## Tests - Add test_integration_config_serialization - Add test_webview_tracking - Add test_token_auth_request_serialization - All 6 integration tests passing ## Files Modified - src-tauri/src/state.rs: Add integration_webviews tracking - src-tauri/src/lib.rs: Register 3 new commands, initialize webviews HashMap - src-tauri/src/commands/integrations.rs: Config persistence, fresh cookie extraction (+151 lines) - src-tauri/src/integrations/webview_auth.rs: Persistent window behavior - src/lib/tauriCommands.ts: TypeScript wrappers for new commands - src/pages/Settings/Integrations.tsx: Load/save configs from DB Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-04-04 14:57:22 +00:00
.inner_size(1000.0, 800.0)
.min_inner_size(800.0, 600.0)
.resizable(true)
.center()
fix: persist integration settings and implement persistent browser windows ## Integration Settings Persistence - Add database commands to save/load integration configs (base_url, username, project_name, space_key) - Frontend now loads configs from DB on mount and saves changes automatically - Fixes issue where settings were lost on app restart ## Persistent Browser Window Architecture - Integration browser windows now stay open for user browsing and authentication - Extract fresh cookies before each API call to handle token rotation - Track open windows in app state (integration_webviews HashMap) - Windows titled as "{Service} Browser (TFTSR)" for clarity - Support easy navigation between app and browser windows (Cmd+Tab/Alt+Tab) - Gracefully handle closed windows with automatic cleanup ## Bug Fixes - Fix Rust formatting issues across 8 files - Fix clippy warnings: - Use is_some_and() instead of map_or() in openai.rs - Use .to_string() instead of format!() in integrations.rs - Add missing OptionalExtension import for .optional() method ## Tests - Add test_integration_config_serialization - Add test_webview_tracking - Add test_token_auth_request_serialization - All 6 integration tests passing ## Files Modified - src-tauri/src/state.rs: Add integration_webviews tracking - src-tauri/src/lib.rs: Register 3 new commands, initialize webviews HashMap - src-tauri/src/commands/integrations.rs: Config persistence, fresh cookie extraction (+151 lines) - src-tauri/src/integrations/webview_auth.rs: Persistent window behavior - src/lib/tauriCommands.ts: TypeScript wrappers for new commands - src/pages/Settings/Integrations.tsx: Load/save configs from DB Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-04-04 14:57:22 +00:00
.focused(true)
.visible(true)
.build()
.map_err(|e| format!("Failed to create webview: {e}"))?;
fix: persist integration settings and implement persistent browser windows ## Integration Settings Persistence - Add database commands to save/load integration configs (base_url, username, project_name, space_key) - Frontend now loads configs from DB on mount and saves changes automatically - Fixes issue where settings were lost on app restart ## Persistent Browser Window Architecture - Integration browser windows now stay open for user browsing and authentication - Extract fresh cookies before each API call to handle token rotation - Track open windows in app state (integration_webviews HashMap) - Windows titled as "{Service} Browser (TFTSR)" for clarity - Support easy navigation between app and browser windows (Cmd+Tab/Alt+Tab) - Gracefully handle closed windows with automatic cleanup ## Bug Fixes - Fix Rust formatting issues across 8 files - Fix clippy warnings: - Use is_some_and() instead of map_or() in openai.rs - Use .to_string() instead of format!() in integrations.rs - Add missing OptionalExtension import for .optional() method ## Tests - Add test_integration_config_serialization - Add test_webview_tracking - Add test_token_auth_request_serialization - All 6 integration tests passing ## Files Modified - src-tauri/src/state.rs: Add integration_webviews tracking - src-tauri/src/lib.rs: Register 3 new commands, initialize webviews HashMap - src-tauri/src/commands/integrations.rs: Config persistence, fresh cookie extraction (+151 lines) - src-tauri/src/integrations/webview_auth.rs: Persistent window behavior - src/lib/tauriCommands.ts: TypeScript wrappers for new commands - src/pages/Settings/Integrations.tsx: Load/save configs from DB Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-04-04 14:57:22 +00:00
// Focus the window
webview
.set_focus()
.map_err(|e| tracing::warn!("Failed to focus webview: {e}"))
fix: persist integration settings and implement persistent browser windows ## Integration Settings Persistence - Add database commands to save/load integration configs (base_url, username, project_name, space_key) - Frontend now loads configs from DB on mount and saves changes automatically - Fixes issue where settings were lost on app restart ## Persistent Browser Window Architecture - Integration browser windows now stay open for user browsing and authentication - Extract fresh cookies before each API call to handle token rotation - Track open windows in app state (integration_webviews HashMap) - Windows titled as "{Service} Browser (TFTSR)" for clarity - Support easy navigation between app and browser windows (Cmd+Tab/Alt+Tab) - Gracefully handle closed windows with automatic cleanup ## Bug Fixes - Fix Rust formatting issues across 8 files - Fix clippy warnings: - Use is_some_and() instead of map_or() in openai.rs - Use .to_string() instead of format!() in integrations.rs - Add missing OptionalExtension import for .optional() method ## Tests - Add test_integration_config_serialization - Add test_webview_tracking - Add test_token_auth_request_serialization - All 6 integration tests passing ## Files Modified - src-tauri/src/state.rs: Add integration_webviews tracking - src-tauri/src/lib.rs: Register 3 new commands, initialize webviews HashMap - src-tauri/src/commands/integrations.rs: Config persistence, fresh cookie extraction (+151 lines) - src-tauri/src/integrations/webview_auth.rs: Persistent window behavior - src/lib/tauriCommands.ts: TypeScript wrappers for new commands - src/pages/Settings/Integrations.tsx: Load/save configs from DB Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-04-04 14:57:22 +00:00
.ok();
// Wait for user to complete login
// User will click "Complete Login" button in the UI after successful authentication
// This function just opens the window - extraction happens in extract_cookies_via_ipc
Ok(ExtractedCredentials {
cookies: vec![],
service: service.to_string(),
})
}
/// Extract cookies from a webview using Tauri's IPC mechanism.
/// This is the most reliable cross-platform approach.
pub async fn extract_cookies_via_ipc<R: tauri::Runtime>(
webview_window: &WebviewWindow<R>,
app_handle: &AppHandle<R>,
) -> Result<Vec<Cookie>, String> {
// Inject JavaScript that will send cookies via IPC
// Note: We use window.__TAURI__ which is the Tauri 2.x API exposed to webviews
let cookie_extraction_script = r#"
(async function() {
try {
// Wait for Tauri API to be available
if (typeof window.__TAURI__ === 'undefined') {
console.error('Tauri API not available');
return;
}
const cookieString = document.cookie;
if (!cookieString || cookieString.trim() === '') {
await window.__TAURI__.event.emit('tftsr-cookies-extracted', { cookies: [] });
return;
}
const cookies = cookieString.split(';').map(c => c.trim()).filter(c => c.length > 0);
const parsed = cookies.map(cookie => {
const equalIndex = cookie.indexOf('=');
if (equalIndex === -1) return null;
const name = cookie.substring(0, equalIndex).trim();
const value = cookie.substring(equalIndex + 1).trim();
return {
name: name,
value: value,
domain: window.location.hostname,
path: '/',
secure: window.location.protocol === 'https:',
http_only: false,
expires: null
};
}).filter(c => c !== null);
// Use Tauri's event API to send cookies back to Rust
await window.__TAURI__.event.emit('tftsr-cookies-extracted', { cookies: parsed });
console.log('Cookies extracted and emitted:', parsed.length);
} catch (e) {
console.error('Cookie extraction failed:', e);
try {
await window.__TAURI__.event.emit('tftsr-cookies-extracted', { cookies: [], error: e.message });
} catch (emitError) {
console.error('Failed to emit error:', emitError);
}
}
})();
"#;
// Set up event listener first
let (tx, mut rx) = tokio::sync::mpsc::channel::<Result<Vec<Cookie>, String>>(1);
// Listen for the custom event from the webview
let listen_id = app_handle.listen("tftsr-cookies-extracted", move |event| {
tracing::debug!("Received cookies-extracted event");
let payload_str = event.payload();
// Parse the payload JSON
match serde_json::from_str::<serde_json::Value>(payload_str) {
Ok(payload) => {
if let Some(error_msg) = payload.get("error").and_then(|e| e.as_str()) {
let _ = tx.try_send(Err(format!("JavaScript error: {error_msg}")));
return;
}
if let Some(cookies_value) = payload.get("cookies") {
match serde_json::from_value::<Vec<Cookie>>(cookies_value.clone()) {
Ok(cookies) => {
tracing::info!("Parsed {} cookies from webview", cookies.len());
let _ = tx.try_send(Ok(cookies));
}
Err(e) => {
tracing::error!("Failed to parse cookies: {e}");
let _ = tx.try_send(Err(format!("Failed to parse cookies: {e}")));
}
}
} else {
let _ = tx.try_send(Err("No cookies field in payload".to_string()));
}
}
Err(e) => {
tracing::error!("Failed to parse event payload: {e}");
let _ = tx.try_send(Err(format!("Failed to parse event payload: {e}")));
}
}
});
// Inject the script into the webview
webview_window
.eval(cookie_extraction_script)
.map_err(|e| format!("Failed to inject cookie extraction script: {e}"))?;
tracing::info!("Cookie extraction script injected, waiting for response...");
// Wait for cookies with timeout
let result = tokio::time::timeout(tokio::time::Duration::from_secs(10), rx.recv())
.await
.map_err(|_| {
"Timeout waiting for cookies. Make sure you are logged in and on the correct page."
.to_string()
})?
.ok_or_else(|| "Failed to receive cookies from webview".to_string())?;
// Clean up event listener
app_handle.unlisten(listen_id);
result
}
/// Build cookie header string for HTTP requests
pub fn cookies_to_header(cookies: &[Cookie]) -> String {
cookies
.iter()
.map(|c| {
format!(
"{name}={value}",
name = c.name.as_str(),
value = c.value.as_str()
)
})
.collect::<Vec<_>>()
.join("; ")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cookies_to_header() {
let cookies = vec![
Cookie {
name: "JSESSIONID".to_string(),
value: "abc123".to_string(),
domain: "example.com".to_string(),
path: "/".to_string(),
secure: true,
http_only: true,
expires: None,
},
Cookie {
name: "auth_token".to_string(),
value: "xyz789".to_string(),
domain: "example.com".to_string(),
path: "/".to_string(),
secure: true,
http_only: false,
expires: None,
},
];
let header = cookies_to_header(&cookies);
assert_eq!(header, "JSESSIONID=abc123; auth_token=xyz789");
}
#[test]
fn test_empty_cookies_to_header() {
let cookies = vec![];
let header = cookies_to_header(&cookies);
assert_eq!(header, "");
}
#[test]
fn test_cookie_json_serialization() {
let cookies = vec![Cookie {
name: "test".to_string(),
value: "value123".to_string(),
domain: "example.com".to_string(),
path: "/".to_string(),
secure: true,
http_only: false,
expires: None,
}];
let json = serde_json::to_string(&cookies).unwrap();
assert!(json.contains("\"name\":\"test\""));
assert!(json.contains("\"value\":\"value123\""));
let deserialized: Vec<Cookie> = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.len(), 1);
assert_eq!(deserialized[0].name, "test");
}
}