use std::time::{Duration, UNIX_EPOCH};
use tauri::State;
use crate::provider::{self, CachedToken, CliProvider, LoginResult};
use crate::session::{self, AppState, ConnectionInfo, ProxyResponse, RequestArgs, Session};
#[derive(serde::Deserialize)]
pub struct HeaderInput {
pub name: String,
pub value: String,
}
fn to_pairs(headers: Vec<HeaderInput>) -> Vec<(String, String)> {
headers
.into_iter()
.filter(|h| !h.name.trim().is_empty())
.map(|h| (h.name.trim().to_string(), h.value))
.collect()
}
fn expires_unix(token: &CachedToken) -> Option<u64> {
token
.expires_at
.and_then(|t| t.duration_since(UNIX_EPOCH).ok())
.map(|d| d.as_secs())
}
fn header_names(headers: &[(String, String)], provider: &Option<CliProvider>) -> Vec<String> {
let mut names: Vec<String> = headers.iter().map(|(n, _)| n.clone()).collect();
if let Some(p) = provider {
names.push(p.header.clone());
}
names
}
async fn provider_header(
state: &AppState,
provider: &CliProvider,
force: bool,
) -> Result<(String, String), String> {
if !force {
let guard = state.token_cache.lock().unwrap();
if let Some(cached) = guard.as_ref() {
if cached.header == provider.header && cached.valid() {
return Ok((cached.header.clone(), cached.value.clone()));
}
}
}
let fresh = provider::fetch_token(provider).await?;
let pair = (fresh.header.clone(), fresh.value.clone());
*state.token_cache.lock().unwrap() = Some(fresh);
Ok(pair)
}
#[tauri::command]
pub async fn connect(
state: State<'_, AppState>,
base_url: String,
api_key: String,
headers: Vec<HeaderInput>,
provider: Option<CliProvider>,
) -> Result<ConnectionInfo, String> {
let base_url = session::normalize_base_url(&base_url);
if base_url.is_empty() {
return Err("Base URL is required.".into());
}
if !base_url.starts_with("http://") && !base_url.starts_with("https://") {
return Err("Base URL must start with http:// or https://".into());
}
let static_headers = to_pairs(headers);
let mut probe_headers = static_headers.clone();
if let Some(p) = &provider {
probe_headers.push(provider_header(state.inner(), p, false).await?);
}
let probe = RequestArgs {
method: "GET".into(),
path: "/".into(),
query: vec![],
body: None,
};
let resp = session::execute(&state.client, &base_url, &api_key, &probe_headers, &probe).await?;
match resp.status {
401 | 403 => {
return Err(format!(
"Authentication failed (HTTP {}). Check the API key and any access headers.",
resp.status
));
}
404 => {
return Err(
"The status endpoint was not found (HTTP 404). Is the base URL pointing at the Hanko Admin API?"
.into(),
);
}
_ => {}
}
let info = ConnectionInfo {
base_url: base_url.clone(),
header_names: header_names(&static_headers, &provider),
};
*state.session.lock().unwrap() = Some(Session {
base_url,
api_key,
headers: static_headers,
provider,
});
Ok(info)
}
#[tauri::command]
pub fn disconnect(state: State<'_, AppState>) {
*state.session.lock().unwrap() = None;
*state.token_cache.lock().unwrap() = None;
}
#[tauri::command]
pub fn is_connected(state: State<'_, AppState>) -> bool {
state.session.lock().unwrap().is_some()
}
#[tauri::command]
pub fn connection_info(state: State<'_, AppState>) -> Option<ConnectionInfo> {
state
.session
.lock()
.unwrap()
.as_ref()
.map(|s| ConnectionInfo {
base_url: s.base_url.clone(),
header_names: header_names(&s.headers, &s.provider),
})
}
#[tauri::command]
pub async fn admin_request(
state: State<'_, AppState>,
args: RequestArgs,
) -> Result<ProxyResponse, String> {
let (base_url, api_key, headers, provider) = {
let guard = state.session.lock().unwrap();
let session = guard.as_ref().ok_or_else(|| {
"Not connected. Please connect to a Hanko Admin API first.".to_string()
})?;
(
session.base_url.clone(),
session.api_key.clone(),
session.headers.clone(),
session.provider.clone(),
)
};
let Some(p) = provider else {
return session::execute(&state.client, &base_url, &api_key, &headers, &args).await;
};
let with_token = |token: (String, String)| {
let mut all = headers.clone();
all.push(token);
all
};
let token = provider_header(state.inner(), &p, false).await?;
let resp = session::execute(
&state.client,
&base_url,
&api_key,
&with_token(token),
&args,
)
.await?;
if resp.status == 401 {
if let Ok(fresh) = provider_header(state.inner(), &p, true).await {
return session::execute(
&state.client,
&base_url,
&api_key,
&with_token(fresh),
&args,
)
.await;
}
}
Ok(resp)
}
#[tauri::command]
pub async fn cli_check(binary: String) -> bool {
provider::run_cli(&binary, &["--version".to_string()], Duration::from_secs(10))
.await
.is_ok()
}
#[tauri::command]
pub async fn cli_login(
state: State<'_, AppState>,
provider: CliProvider,
) -> Result<LoginResult, String> {
let token = provider::login(&provider).await?;
let expires_at = expires_unix(&token);
*state.token_cache.lock().unwrap() = Some(token);
Ok(LoginResult { expires_at })
}