use std::sync::Arc;
use axum::Extension;
use axum::Json;
use axum::extract::{Path, Query};
use axum::http::StatusCode;
use axum::response::IntoResponse;
use serde::Deserialize;
use serde_json::Value as JsonValue;
use tracing::field::Empty;
use uuid::Uuid;
use toolkit::api::odata::OData;
use toolkit::api::response::JsonPage;
use toolkit_security::SecurityContext;
use crate::domain::error::{ChatEngineError, Result};
use crate::domain::service::session_service::{
CreateSessionRequest, Identity, SessionDeleteOutcome, SessionService,
};
#[derive(Debug, Deserialize)]
pub struct CreateSessionBody {
pub session_type_id: Option<Uuid>,
pub metadata: Option<JsonValue>,
pub tenant_id: Option<JsonValue>,
pub user_id: Option<JsonValue>,
}
#[derive(Debug, Deserialize)]
pub struct PatchSessionBody {
pub metadata: Option<JsonValue>,
pub enabled_capabilities: Option<Vec<chat_engine_sdk::models::CapabilityValue>>,
pub tenant_id: Option<JsonValue>,
pub user_id: Option<JsonValue>,
}
#[derive(Debug, Deserialize, Default)]
pub struct DeleteSessionQuery {
#[serde(default)]
pub hard: Option<bool>,
}
#[tracing::instrument(
skip(svc, ctx, body),
fields(request_id = Empty, session_id = Empty),
)]
pub async fn create_session(
Extension(ctx): Extension<SecurityContext>,
Extension(svc): Extension<Arc<SessionService>>,
Json(body): Json<CreateSessionBody>,
) -> Result<impl IntoResponse> {
reject_body_identity(&body.tenant_id, &body.user_id)?;
let identity = identity_from_ctx(&ctx)?;
let session = svc
.create_session(
&identity,
CreateSessionRequest {
session_type_id: body.session_type_id,
metadata: body.metadata,
},
)
.await?;
Ok((StatusCode::CREATED, Json(session)))
}
#[tracing::instrument(skip(svc, ctx, query), fields(request_id = Empty))]
pub async fn list_sessions(
Extension(ctx): Extension<SecurityContext>,
Extension(svc): Extension<Arc<SessionService>>,
OData(query): OData,
) -> Result<JsonPage<chat_engine_sdk::models::Session>> {
let identity = identity_from_ctx(&ctx)?;
let page = svc.list_sessions(&identity, &query).await?;
Ok(Json(page))
}
#[tracing::instrument(skip(svc, ctx), fields(session_id = %session_id))]
pub async fn get_session(
Extension(ctx): Extension<SecurityContext>,
Extension(svc): Extension<Arc<SessionService>>,
Path(session_id): Path<Uuid>,
) -> Result<Json<chat_engine_sdk::models::Session>> {
let identity = identity_from_ctx(&ctx)?;
let session = svc.get_session(&identity, session_id).await?;
Ok(Json(session))
}
#[tracing::instrument(skip(svc, ctx, body), fields(session_id = %session_id))]
pub async fn patch_session(
Extension(ctx): Extension<SecurityContext>,
Extension(svc): Extension<Arc<SessionService>>,
Path(session_id): Path<Uuid>,
Json(body): Json<PatchSessionBody>,
) -> Result<Json<chat_engine_sdk::models::Session>> {
reject_body_identity(&body.tenant_id, &body.user_id)?;
if body.metadata.is_none() && body.enabled_capabilities.is_none() {
return Err(ChatEngineError::bad_request(
"request must supply at least one of `metadata` or `enabled_capabilities`",
));
}
let identity = identity_from_ctx(&ctx)?;
let mut latest: Option<chat_engine_sdk::models::Session> = None;
if let Some(metadata) = body.metadata {
latest = Some(svc.update_metadata(&identity, session_id, metadata).await?);
}
if let Some(caps) = body.enabled_capabilities {
latest = Some(svc.update_capabilities(&identity, session_id, caps).await?);
}
latest
.map(Json)
.ok_or_else(|| ChatEngineError::internal("patch produced no session update"))
}
#[tracing::instrument(skip(svc, ctx), fields(session_id = %session_id))]
pub async fn delete_session(
Extension(ctx): Extension<SecurityContext>,
Extension(svc): Extension<Arc<SessionService>>,
Path(session_id): Path<Uuid>,
Query(query): Query<DeleteSessionQuery>,
) -> Result<axum::response::Response> {
let identity = identity_from_ctx(&ctx)?;
let hard = query.hard.unwrap_or(false);
let outcome = svc.delete_session(&identity, session_id, hard).await?;
match outcome {
SessionDeleteOutcome::Soft { session } => {
Ok((StatusCode::OK, Json(session)).into_response())
}
SessionDeleteOutcome::Hard => Ok(StatusCode::NO_CONTENT.into_response()),
}
}
#[tracing::instrument(skip(svc, ctx), fields(session_id = %session_id))]
pub async fn archive_session(
Extension(ctx): Extension<SecurityContext>,
Extension(svc): Extension<Arc<SessionService>>,
Path(session_id): Path<Uuid>,
) -> Result<Json<chat_engine_sdk::models::Session>> {
let identity = identity_from_ctx(&ctx)?;
let session = svc.archive_session(&identity, session_id).await?;
Ok(Json(session))
}
#[tracing::instrument(skip(svc, ctx), fields(session_id = %session_id))]
pub async fn restore_session(
Extension(ctx): Extension<SecurityContext>,
Extension(svc): Extension<Arc<SessionService>>,
Path(session_id): Path<Uuid>,
) -> Result<Json<chat_engine_sdk::models::Session>> {
let identity = identity_from_ctx(&ctx)?;
let session = svc.restore_session(&identity, session_id).await?;
Ok(Json(session))
}
pub(crate) fn identity_from_ctx(ctx: &SecurityContext) -> Result<Identity> {
let tenant = ctx.subject_tenant_id();
let user = ctx.subject_id();
if tenant.is_nil() || user.is_nil() {
return Err(ChatEngineError::forbidden(
"authenticated identity required (tenant_id and user_id must be present in the bearer token)",
));
}
Identity::new(tenant.to_string(), user.to_string(), None)
}
#[allow(clippy::ref_option)]
pub(crate) fn reject_body_identity(
tenant_id: &Option<JsonValue>,
user_id: &Option<JsonValue>,
) -> Result<()> {
if tenant_id.is_some() || user_id.is_some() {
return Err(ChatEngineError::bad_request(
"tenant_id / user_id are derived from the auth token and must not appear in the request body",
));
}
Ok(())
}