1use crate::{
6 auth::middleware::AuthUser,
7 db::postgres::Conversation,
8 types::{AppError, Result},
9 AppState,
10};
11use axum::{
12 extract::{Path, State},
13 Json,
14};
15use serde::{Deserialize, Serialize};
16use utoipa::ToSchema;
17
18#[derive(Debug, Serialize, ToSchema)]
20pub struct ConversationSummary {
21 pub id: String,
23 pub title: Option<String>,
25 pub message_count: i32,
27 pub created_at: String,
29 pub updated_at: String,
31}
32
33impl From<Conversation> for ConversationSummary {
34 fn from(c: Conversation) -> Self {
35 Self {
36 id: c.id,
37 title: c.title,
38 message_count: c.message_count,
39 created_at: c.created_at,
40 updated_at: c.updated_at,
41 }
42 }
43}
44
45#[derive(Debug, Serialize, ToSchema)]
47pub struct ConversationDetails {
48 pub id: String,
50 pub title: Option<String>,
52 pub messages: Vec<ConversationMessage>,
54 pub created_at: String,
56 pub updated_at: String,
58}
59
60#[derive(Debug, Serialize, ToSchema)]
62pub struct ConversationMessage {
63 pub id: String,
65 pub role: String,
67 pub content: String,
69 pub created_at: String,
71}
72
73#[derive(Debug, Deserialize, ToSchema)]
75pub struct UpdateConversationRequest {
76 pub title: Option<String>,
78}
79
80#[utoipa::path(
82 get,
83 path = "/api/conversations",
84 responses(
85 (status = 200, description = "List of conversations", body = Vec<ConversationSummary>),
86 (status = 401, description = "Unauthorized")
87 ),
88 tag = "conversations",
89 security(("bearer" = []))
90)]
91pub async fn list_conversations(
92 State(state): State<AppState>,
93 AuthUser(claims): AuthUser,
94) -> Result<Json<Vec<ConversationSummary>>> {
95 let conversations = state.db.get_user_conversations(&claims.sub).await?;
96
97 let summaries: Vec<ConversationSummary> = conversations
98 .into_iter()
99 .map(|c| ConversationSummary {
100 id: c.id,
101 title: Some(c.title),
102 message_count: c.message_count,
103 created_at: c.created_at,
104 updated_at: c.updated_at,
105 })
106 .collect();
107
108 Ok(Json(summaries))
109}
110
111#[utoipa::path(
113 get,
114 path = "/api/conversations/{id}",
115 params(
116 ("id" = String, Path, description = "Conversation ID")
117 ),
118 responses(
119 (status = 200, description = "Conversation details", body = ConversationDetails),
120 (status = 404, description = "Conversation not found"),
121 (status = 401, description = "Unauthorized")
122 ),
123 tag = "conversations",
124 security(("bearer" = []))
125)]
126pub async fn get_conversation(
127 State(state): State<AppState>,
128 AuthUser(claims): AuthUser,
129 Path(id): Path<String>,
130) -> Result<Json<ConversationDetails>> {
131 let conversation = state.db.get_conversation(&id).await?;
133
134 if conversation.user_id != claims.sub {
135 return Err(AppError::Auth(
136 "Not authorized to access this conversation".to_string(),
137 ));
138 }
139
140 let messages = state.db.get_conversation_history(&id).await?;
141
142 let message_details: Vec<ConversationMessage> = messages
143 .into_iter()
144 .enumerate()
145 .map(|(idx, msg)| ConversationMessage {
146 id: format!("{}-{}", id, idx), role: format!("{:?}", msg.role).to_lowercase(),
148 content: msg.content,
149 created_at: msg.timestamp.to_rfc3339(),
150 })
151 .collect();
152
153 Ok(Json(ConversationDetails {
154 id: conversation.id,
155 title: conversation.title,
156 messages: message_details,
157 created_at: conversation.created_at,
158 updated_at: conversation.updated_at,
159 }))
160}
161
162#[utoipa::path(
164 put,
165 path = "/api/conversations/{id}",
166 params(
167 ("id" = String, Path, description = "Conversation ID")
168 ),
169 request_body = UpdateConversationRequest,
170 responses(
171 (status = 200, description = "Conversation updated"),
172 (status = 404, description = "Conversation not found"),
173 (status = 401, description = "Unauthorized")
174 ),
175 tag = "conversations",
176 security(("bearer" = []))
177)]
178pub async fn update_conversation(
179 State(state): State<AppState>,
180 AuthUser(claims): AuthUser,
181 Path(id): Path<String>,
182 Json(payload): Json<UpdateConversationRequest>,
183) -> Result<Json<serde_json::Value>> {
184 let conversation = state.db.get_conversation(&id).await?;
186
187 if conversation.user_id != claims.sub {
188 return Err(AppError::Auth(
189 "Not authorized to modify this conversation".to_string(),
190 ));
191 }
192
193 state
194 .db
195 .update_conversation_title(&id, payload.title.as_deref())
196 .await?;
197
198 Ok(Json(serde_json::json!({"success": true})))
199}
200
201#[utoipa::path(
203 delete,
204 path = "/api/conversations/{id}",
205 params(
206 ("id" = String, Path, description = "Conversation ID")
207 ),
208 responses(
209 (status = 204, description = "Conversation deleted"),
210 (status = 404, description = "Conversation not found"),
211 (status = 401, description = "Unauthorized")
212 ),
213 tag = "conversations",
214 security(("bearer" = []))
215)]
216pub async fn delete_conversation(
217 State(state): State<AppState>,
218 AuthUser(claims): AuthUser,
219 Path(id): Path<String>,
220) -> Result<axum::http::StatusCode> {
221 let conversation = state.db.get_conversation(&id).await?;
223
224 if conversation.user_id != claims.sub {
225 return Err(AppError::Auth(
226 "Not authorized to delete this conversation".to_string(),
227 ));
228 }
229
230 state.db.delete_conversation(&id).await?;
231
232 Ok(axum::http::StatusCode::NO_CONTENT)
233}