use crate::{
auth::middleware::AuthUser,
db::postgres::Conversation,
types::{AppError, Result},
AppState,
};
use axum::{
extract::{Path, State},
Json,
};
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
#[derive(Debug, Serialize, ToSchema)]
pub struct ConversationSummary {
pub id: String,
pub title: Option<String>,
pub message_count: i32,
pub created_at: String,
pub updated_at: String,
}
impl From<Conversation> for ConversationSummary {
fn from(c: Conversation) -> Self {
Self {
id: c.id,
title: c.title,
message_count: c.message_count,
created_at: c.created_at,
updated_at: c.updated_at,
}
}
}
#[derive(Debug, Serialize, ToSchema)]
pub struct ConversationDetails {
pub id: String,
pub title: Option<String>,
pub messages: Vec<ConversationMessage>,
pub created_at: String,
pub updated_at: String,
}
#[derive(Debug, Serialize, ToSchema)]
pub struct ConversationMessage {
pub id: String,
pub role: String,
pub content: String,
pub created_at: String,
}
#[derive(Debug, Deserialize, ToSchema)]
pub struct UpdateConversationRequest {
pub title: Option<String>,
}
#[utoipa::path(
get,
path = "/api/conversations",
responses(
(status = 200, description = "List of conversations", body = Vec<ConversationSummary>),
(status = 401, description = "Unauthorized")
),
tag = "conversations",
security(("bearer" = []))
)]
pub async fn list_conversations(
State(state): State<AppState>,
AuthUser(claims): AuthUser,
) -> Result<Json<Vec<ConversationSummary>>> {
let conversations = state.db.get_user_conversations(&claims.sub).await?;
let summaries: Vec<ConversationSummary> = conversations
.into_iter()
.map(|c| ConversationSummary { id: c.id, title: Some(c.title), message_count: c.message_count, created_at: c.created_at, updated_at: c.updated_at })
.collect();
Ok(Json(summaries))
}
#[utoipa::path(
get,
path = "/api/conversations/{id}",
params(
("id" = String, Path, description = "Conversation ID")
),
responses(
(status = 200, description = "Conversation details", body = ConversationDetails),
(status = 404, description = "Conversation not found"),
(status = 401, description = "Unauthorized")
),
tag = "conversations",
security(("bearer" = []))
)]
pub async fn get_conversation(
State(state): State<AppState>,
AuthUser(claims): AuthUser,
Path(id): Path<String>,
) -> Result<Json<ConversationDetails>> {
let conversation = state.db.get_conversation(&id).await?;
if conversation.user_id != claims.sub {
return Err(AppError::Auth(
"Not authorized to access this conversation".to_string(),
));
}
let messages = state.db.get_conversation_history(&id).await?;
let message_details: Vec<ConversationMessage> = messages
.into_iter()
.enumerate()
.map(|(idx, msg)| ConversationMessage {
id: format!("{}-{}", id, idx), role: format!("{:?}", msg.role).to_lowercase(),
content: msg.content,
created_at: msg.timestamp.to_rfc3339(),
})
.collect();
Ok(Json(ConversationDetails {
id: conversation.id,
title: conversation.title,
messages: message_details,
created_at: conversation.created_at,
updated_at: conversation.updated_at,
}))
}
#[utoipa::path(
put,
path = "/api/conversations/{id}",
params(
("id" = String, Path, description = "Conversation ID")
),
request_body = UpdateConversationRequest,
responses(
(status = 200, description = "Conversation updated"),
(status = 404, description = "Conversation not found"),
(status = 401, description = "Unauthorized")
),
tag = "conversations",
security(("bearer" = []))
)]
pub async fn update_conversation(
State(state): State<AppState>,
AuthUser(claims): AuthUser,
Path(id): Path<String>,
Json(payload): Json<UpdateConversationRequest>,
) -> Result<Json<serde_json::Value>> {
let conversation = state.db.get_conversation(&id).await?;
if conversation.user_id != claims.sub {
return Err(AppError::Auth(
"Not authorized to modify this conversation".to_string(),
));
}
state
.db
.update_conversation_title(&id, payload.title.as_deref())
.await?;
Ok(Json(serde_json::json!({"success": true})))
}
#[utoipa::path(
delete,
path = "/api/conversations/{id}",
params(
("id" = String, Path, description = "Conversation ID")
),
responses(
(status = 204, description = "Conversation deleted"),
(status = 404, description = "Conversation not found"),
(status = 401, description = "Unauthorized")
),
tag = "conversations",
security(("bearer" = []))
)]
pub async fn delete_conversation(
State(state): State<AppState>,
AuthUser(claims): AuthUser,
Path(id): Path<String>,
) -> Result<axum::http::StatusCode> {
let conversation = state.db.get_conversation(&id).await?;
if conversation.user_id != claims.sub {
return Err(AppError::Auth(
"Not authorized to delete this conversation".to_string(),
));
}
state.db.delete_conversation(&id).await?;
Ok(axum::http::StatusCode::NO_CONTENT)
}