use std::sync::Arc;
use axum::Extension;
use axum::Json;
use axum::extract::{Path, Query};
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
use tracing::field::Empty;
use uuid::Uuid;
use toolkit_security::SecurityContext;
use crate::api::rest::handlers::sessions::{identity_from_ctx, reject_body_identity};
use crate::domain::error::{ChatEngineError, Result};
use crate::domain::export::{ExportFormat, ExportedSession, ShareTokenIssue, SharedSessionView};
use crate::domain::service::export_service::{ExportService, is_share_token_expired};
#[derive(Debug, Deserialize, Default)]
pub struct ExportSessionQuery {
pub format: Option<String>,
#[serde(default)]
pub include_plugin_metadata: Option<bool>,
}
#[derive(Debug, Deserialize, Default)]
pub struct CreateShareBody {
pub expires_in_hours: Option<u32>,
#[serde(default)]
pub tenant_id: Option<JsonValue>,
#[serde(default)]
pub user_id: Option<JsonValue>,
}
pub type ExportSessionResponse = ExportedSession;
pub type CreateShareResponse = ShareTokenIssue;
pub type SharedSessionResponse = SharedSessionView;
#[derive(Debug, Serialize)]
pub struct RevokeShareResponse {}
#[tracing::instrument(
skip(svc, ctx),
fields(session_id = %session_id, format = Empty, message_count = Empty),
)]
pub async fn export_session(
Extension(ctx): Extension<SecurityContext>,
Extension(svc): Extension<Arc<ExportService>>,
Path(session_id): Path<Uuid>,
Query(query): Query<ExportSessionQuery>,
) -> Result<Json<ExportSessionResponse>> {
let identity = identity_from_ctx(&ctx)?;
let format = parse_format(query.format.as_deref())?;
tracing::Span::current().record("format", tracing::field::display(format.as_str()));
let exported = svc
.export(
&identity,
session_id,
format,
query.include_plugin_metadata.unwrap_or(false),
)
.await?;
tracing::Span::current().record("message_count", exported.message_count);
Ok(Json(exported))
}
#[tracing::instrument(
skip(svc, ctx, body),
fields(session_id = %session_id),
)]
pub async fn create_share(
Extension(ctx): Extension<SecurityContext>,
Extension(svc): Extension<Arc<ExportService>>,
Path(session_id): Path<Uuid>,
Json(body): Json<CreateShareBody>,
) -> Result<(StatusCode, Json<CreateShareResponse>)> {
reject_body_identity(&body.tenant_id, &body.user_id)?;
let identity = identity_from_ctx(&ctx)?;
let issue = svc
.create_share(&identity, session_id, body.expires_in_hours)
.await?;
Ok((StatusCode::CREATED, Json(issue)))
}
#[tracing::instrument(
skip(svc, ctx),
fields(session_id = %session_id),
)]
pub async fn revoke_share(
Extension(ctx): Extension<SecurityContext>,
Extension(svc): Extension<Arc<ExportService>>,
Path(session_id): Path<Uuid>,
) -> Result<Json<RevokeShareResponse>> {
let identity = identity_from_ctx(&ctx)?;
svc.revoke_share(&identity, session_id).await?;
Ok(Json(RevokeShareResponse {}))
}
#[tracing::instrument(skip(svc, token), fields(token = "***redacted***"))]
pub async fn access_shared(
Extension(svc): Extension<Arc<ExportService>>,
Path(token): Path<String>,
) -> Response {
match svc.access_shared(&token).await {
Ok(view) => Json::<SharedSessionResponse>(view).into_response(),
Err(err) => map_share_error(err),
}
}
fn parse_format(raw: Option<&str>) -> Result<ExportFormat> {
use std::str::FromStr;
match raw {
None => Ok(ExportFormat::Json),
Some(s) => ExportFormat::from_str(s),
}
}
fn map_share_error(err: ChatEngineError) -> Response {
if is_share_token_expired(&err) {
return (
StatusCode::GONE,
Json(serde_json::json!({"error": "share_token_expired"})),
)
.into_response();
}
err.into_response()
}
#[cfg(test)]
#[path = "export_tests.rs"]
mod export_tests;