use std::sync::Arc;
use axum::{
Json,
extract::{Path, State},
};
use serde::{Deserialize, Serialize};
use crate::api::error::AppError;
use crate::api::server::AppState;
use oxios_kernel::CredentialStatus;
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct IntegrationRow {
pub id: String,
pub label: String,
pub cli: Option<String>,
pub resolver_kind: String,
pub detected: Option<DetectedRow>,
pub credential: CredentialStatus,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DetectedRow {
pub installed: bool,
pub version: Option<String>,
pub source: String,
pub path: String,
}
pub(crate) async fn handle_integrations_list(
state: State<Arc<AppState>>,
) -> Result<Json<Vec<IntegrationRow>>, AppError> {
let mut rows = Vec::new();
for it in state.kernel.host_tools.integrations() {
let detected = if let Some(cli) = &it.cli {
match state.kernel.host_tools.detect(cli).await {
Some(t) => Some(DetectedRow {
installed: true,
version: t.version,
source: format!("{:?}", t.source).to_lowercase(),
path: t.path,
}),
None => Some(DetectedRow {
installed: false,
version: None,
source: "none".into(),
path: String::new(),
}),
}
} else {
None
};
let credential =
state
.kernel
.host_tools
.credential_status(&it.id)
.unwrap_or(CredentialStatus {
configured: false,
source: "none".into(),
});
rows.push(IntegrationRow {
id: it.id.clone(),
label: it.label.clone(),
cli: it.cli.clone(),
resolver_kind: resolver_kind_label(&it.credential),
detected,
credential,
});
}
Ok(Json(rows))
}
pub(crate) async fn handle_integration_credential_status(
state: State<Arc<AppState>>,
Path(id): Path<String>,
) -> Result<Json<CredentialStatus>, AppError> {
let status = state
.kernel
.host_tools
.credential_status(&id)
.ok_or_else(|| AppError::NotFound(format!("integration '{id}' not found")))?;
Ok(Json(status))
}
pub(crate) async fn handle_integration_install(
state: State<Arc<AppState>>,
Path(id): Path<String>,
) -> Result<Json<oxios_kernel::host_tools::InstallOutput>, AppError> {
let out = state
.kernel
.host_tools
.install(&id)
.await
.map_err(|e| AppError::Internal(format!("install failed: {e}")))?;
Ok(Json(out))
}
pub(crate) async fn handle_integration_oauth_start(
state: State<Arc<AppState>>,
Path(id): Path<String>,
) -> Result<Json<oxios_kernel::host_tools::DeviceCodeResponse>, AppError> {
let resp = state
.kernel
.host_tools
.oauth_start(&id)
.await
.map_err(|e| AppError::Internal(format!("oauth start failed: {e}")))?;
Ok(Json(resp))
}
pub(crate) async fn handle_integration_oauth_poll(
state: State<Arc<AppState>>,
Path(id): Path<String>,
axum::extract::Query(q): axum::extract::Query<OAuthPollQuery>,
) -> Result<Json<oxios_kernel::host_tools::PollResponse>, AppError> {
let _ = id; let resp = state
.kernel
.host_tools
.oauth_poll(&q.handle)
.await
.map_err(|e| AppError::BadRequest(format!("oauth poll failed: {e}")))?;
Ok(Json(resp))
}
#[derive(Debug, Deserialize)]
pub struct OAuthPollQuery {
pub handle: String,
}
#[derive(Debug, Deserialize)]
pub struct SetCredentialBody {
pub value: String,
}
pub(crate) async fn handle_integration_credential_set(
state: State<Arc<AppState>>,
Path(id): Path<String>,
Json(body): Json<SetCredentialBody>,
) -> Result<Json<serde_json::Value>, AppError> {
use oxios_kernel::host_tools::CredentialResolver;
let it = state
.kernel
.host_tools
.integration(&id)
.ok_or_else(|| AppError::NotFound(format!("integration '{id}' not found")))?;
let store_key = match &it.credential {
CredentialResolver::Secret { store_key, .. } => store_key.clone(),
other => {
return Err(AppError::BadRequest(format!(
"integration '{id}' uses {:?} resolver; only 'secret' is settable here",
other
)));
}
};
oxios_kernel::CredentialStore::store(&store_key, &body.value)
.map_err(|e| AppError::Internal(format!("failed to store credential: {e}")))?;
Ok(Json(serde_json::json!({ "status": "ok", "id": id })))
}
pub(crate) async fn handle_integration_credential_delete(
state: State<Arc<AppState>>,
Path(id): Path<String>,
) -> Result<Json<serde_json::Value>, AppError> {
use oxios_kernel::host_tools::CredentialResolver;
let it = state
.kernel
.host_tools
.integration(&id)
.ok_or_else(|| AppError::NotFound(format!("integration '{id}' not found")))?;
let store_key = match &it.credential {
CredentialResolver::Secret { store_key, .. } => store_key.clone(),
CredentialResolver::OAuth { store_key, .. } => {
store_key.clone()
}
other => {
return Err(AppError::BadRequest(format!(
"integration '{id}' uses {:?} resolver; nothing to delete",
other
)));
}
};
oxios_kernel::CredentialStore::delete(&store_key)
.map_err(|e| AppError::Internal(format!("failed to delete credential: {e}")))?;
Ok(Json(serde_json::json!({ "status": "ok", "id": id })))
}
fn resolver_kind_label(c: &oxios_kernel::host_tools::CredentialResolver) -> String {
use oxios_kernel::host_tools::CredentialResolver;
match c {
CredentialResolver::None => "none",
CredentialResolver::Secret { .. } => "secret",
CredentialResolver::OAuth { .. } => "oauth",
}
.into()
}