diff --git a/api/libs/database/src/models.rs b/api/libs/database/src/models.rs index fcf64e439..6d2a9bbe2 100644 --- a/api/libs/database/src/models.rs +++ b/api/libs/database/src/models.rs @@ -56,6 +56,7 @@ pub struct Message { pub updated_at: DateTime, pub deleted_at: Option>, pub created_by: Uuid, + pub feedback: Option, } #[derive(Queryable, Insertable, Debug)] diff --git a/api/libs/database/src/schema.rs b/api/libs/database/src/schema.rs index f2efbe2fd..704b5d036 100644 --- a/api/libs/database/src/schema.rs +++ b/api/libs/database/src/schema.rs @@ -334,6 +334,7 @@ diesel::table! { updated_at -> Timestamptz, deleted_at -> Nullable, created_by -> Uuid, + feedback -> Nullable, } } diff --git a/api/libs/handlers/src/chats/post_chat_handler.rs b/api/libs/handlers/src/chats/post_chat_handler.rs index d63d78de7..dd2e4bdff 100644 --- a/api/libs/handlers/src/chats/post_chat_handler.rs +++ b/api/libs/handlers/src/chats/post_chat_handler.rs @@ -369,6 +369,7 @@ pub async fn post_chat_handler( final_reasoning_message: format!("Reasoned for {} seconds", reasoning_duration), title: title.title.clone().unwrap_or_default(), raw_llm_messages: serde_json::to_value(&raw_llm_messages)?, + feedback: None }; let mut conn = get_pg_pool().get().await?; diff --git a/api/libs/handlers/src/messages/helpers/mod.rs b/api/libs/handlers/src/messages/helpers/mod.rs index 2c762b60a..47712db58 100644 --- a/api/libs/handlers/src/messages/helpers/mod.rs +++ b/api/libs/handlers/src/messages/helpers/mod.rs @@ -1,3 +1,5 @@ pub mod delete_message_handler; +pub mod update_message_handler; pub use delete_message_handler::*; +pub use update_message_handler::*; diff --git a/api/libs/handlers/src/messages/helpers/update_message_handler.rs b/api/libs/handlers/src/messages/helpers/update_message_handler.rs new file mode 100644 index 000000000..5303806b9 --- /dev/null +++ b/api/libs/handlers/src/messages/helpers/update_message_handler.rs @@ -0,0 +1,72 @@ +use anyhow::{anyhow, Result}; +use chrono::Utc; +use database::{pool::get_pg_pool, schema::messages}; +use diesel::prelude::*; +use diesel_async::RunQueryDsl; +use middleware::AuthenticatedUser; +use serde_json::{json, Value}; +use std::str::FromStr; +use uuid::Uuid; + +use crate::messages::types::MessageFeedback; + +/// Update a message with new properties +/// +/// # Arguments +/// * `user` - The authenticated user +/// * `message_id` - The ID of the message to update +/// * `feedback` - Optional feedback for the message ("positive" or "negative") +/// +/// # Returns +/// * `Ok(())` - If the message was successfully updated +/// * `Err(anyhow::Error)` - If there was an error updating the message +pub async fn update_message_handler( + user: AuthenticatedUser, + message_id: Uuid, + feedback: Option, +) -> Result<()> { + let pool = get_pg_pool(); + let mut conn = pool.get().await?; + + // Check if the message exists and belongs to the user + let message_exists = diesel::dsl::select(diesel::dsl::exists( + messages::table.filter( + messages::id.eq(message_id) + .and(messages::created_by.eq(user.id)), + ), + )) + .get_result::(&mut conn) + .await?; + + if !message_exists { + return Err(anyhow!("Message not found or you don't have permission to update it")); + } + + // Build update parameters - don't execute the set operation yet + let update_statement = diesel::update(messages::table) + .filter(messages::id.eq(message_id)); + + // Add feedback if provided + if let Some(fb_str) = feedback { + // Validate feedback value + let feedback = MessageFeedback::from_str(&fb_str) + .map_err(|e| anyhow!(e))?; + + // Update the feedback column directly + update_statement + .set(( + messages::updated_at.eq(Utc::now()), + messages::feedback.eq(feedback.to_string()) + )) + .execute(&mut conn) + .await?; + } else { + // If no feedback, just update the timestamp + update_statement + .set(messages::updated_at.eq(Utc::now())) + .execute(&mut conn) + .await?; + } + + Ok(()) +} diff --git a/api/libs/handlers/src/messages/types.rs b/api/libs/handlers/src/messages/types.rs index 07f9ac092..fd4feddcd 100644 --- a/api/libs/handlers/src/messages/types.rs +++ b/api/libs/handlers/src/messages/types.rs @@ -5,6 +5,9 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; use uuid::Uuid; +pub mod message_feedback; +pub use message_feedback::MessageFeedback; + #[derive(Debug, Serialize, Deserialize, Clone)] pub struct ChatMessage { pub id: Uuid, diff --git a/api/libs/handlers/src/messages/types/message_feedback.rs b/api/libs/handlers/src/messages/types/message_feedback.rs new file mode 100644 index 000000000..c6daa6c50 --- /dev/null +++ b/api/libs/handlers/src/messages/types/message_feedback.rs @@ -0,0 +1,32 @@ +use serde::{Deserialize, Serialize}; + +/// Represents the feedback type for a message +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum MessageFeedback { + /// Positive feedback + Positive, + /// Negative feedback + Negative, +} + +impl std::fmt::Display for MessageFeedback { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + MessageFeedback::Positive => write!(f, "positive"), + MessageFeedback::Negative => write!(f, "negative"), + } + } +} + +impl std::str::FromStr for MessageFeedback { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "positive" => Ok(MessageFeedback::Positive), + "negative" => Ok(MessageFeedback::Negative), + _ => Err("Invalid feedback value. Must be 'positive' or 'negative'"), + } + } +} diff --git a/api/migrations/2025-03-20-220600_add_feedback_to_message/down.sql b/api/migrations/2025-03-20-220600_add_feedback_to_message/down.sql new file mode 100644 index 000000000..ea614fa35 --- /dev/null +++ b/api/migrations/2025-03-20-220600_add_feedback_to_message/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +alter table messages drop column feedback; diff --git a/api/migrations/2025-03-20-220600_add_feedback_to_message/up.sql b/api/migrations/2025-03-20-220600_add_feedback_to_message/up.sql new file mode 100644 index 000000000..d0abebca5 --- /dev/null +++ b/api/migrations/2025-03-20-220600_add_feedback_to_message/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +alter table messages add column feedback text null; \ No newline at end of file diff --git a/api/prds/active/api_update_message_endpoint.md b/api/prds/active/api_update_message_endpoint.md new file mode 100644 index 000000000..068c72617 --- /dev/null +++ b/api/prds/active/api_update_message_endpoint.md @@ -0,0 +1,118 @@ +# Update Message REST Endpoint PRD + +## Overview +This PRD describes the implementation of a new REST endpoint for updating a message. This endpoint will allow users to update specific properties of a message, such as setting feedback on a message. + +## Endpoint Details + +### Basic Information +- **HTTP Method**: PUT +- **URL Path**: `/api/v1/messages/:id` +- **Purpose**: Update properties of a specific message + +### Request Format +- **Headers**: + - `Content-Type: application/json` + - `Authorization: Bearer {token}` +- **URL Parameters**: + - `id`: UUID of the message to update +- **Request Body (JSON)**: + - `feedback`: String (optional) - "positive" or "negative" + +### Response Format +- **Success**: + - Status: 200 OK + - Body: Updated message object +- **Error Responses**: + - 401 Unauthorized: If the user is not authenticated + - 403 Forbidden: If the user does not have permission to update the message + - 404 Not Found: If the message with the specified ID does not exist + - 500 Internal Server Error: If there's a server error + +### Example Request +```bash +curl --location --request PUT 'http://localhost:3001/api/v1/messages/cee3065d-c165-4e04-be44-be05d1e6d902' \ +--header 'Content-Type: application/json' \ +--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJjMmRkNjRjZC1mN2YzLTQ4ODQtYmM5MS1kNDZhZTQzMTkwMWUiLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE1MTYyMzkwMjIsImV4cCI6MjUxODU1NTMyNiwiYXVkIjoiYXV0aGVudGljYXRlZCJ9.uRs5OVyYErQ1iSQwAXVUD6TFolOu31ejPhcBS41ResA' \ +--data '{ + "feedback": "negative" +}' +``` + +### Example Response +```json +{ + "id": "cee3065d-c165-4e04-be44-be05d1e6d902", + "request_message": { + "request": "Example message text", + "sender_id": "c2dd64cd-f7f3-4884-bc91-d46ae431901e", + "sender_name": "John Doe", + "sender_avatar": null + }, + "response_message_ids": ["..."], + "response_messages": {}, + "reasoning_message_ids": ["..."], + "reasoning_messages": {}, + "created_at": "2025-03-20T15:30:00Z", + "final_reasoning_message": null, + "feedback": "negative" +} +``` + +## Implementation Details + +### New Files to Create + +#### 1. Handler Implementation +- **File**: `/libs/handlers/src/messages/helpers/update_message_handler.rs` +- **Purpose**: Contains the business logic for updating a message + +#### 2. REST Route Implementation +- **File**: `/src/routes/rest/routes/messages/update_message.rs` +- **Purpose**: Contains the HTTP handler for the REST endpoint + +#### 3. Update Module Exports +- **File**: `/libs/handlers/src/messages/helpers/mod.rs` (update) +- **File**: `/src/routes/rest/routes/messages/mod.rs` (update) + +### Implementation Steps + +#### 1. Create the Handler Function +Create a new handler in `/libs/handlers/src/messages/helpers/update_message_handler.rs` that: +- Takes a user, message ID, and update parameters +- Verifies the user has permission to update the message +- Updates the message in the database +- Returns the updated message + +#### 2. Create the REST Endpoint +Create a new file at `/src/routes/rest/routes/messages/update_message.rs` that: +- Creates a request struct to deserialize the request body +- Maps the HTTP request to the handler function +- Handles errors appropriately +- Returns the appropriate HTTP response + +#### 3. Update Module Exports +Update the module exports to include the new handler and endpoint. + +## Technical Details + +### Database Updates +The update operation will modify records in the `messages` table. + +### Expected Behavior +- Only the user who created the message or an admin should be able to update the message +- Updates should be atomic and consistent +- The endpoint should validate input data before updating the database + +### Testing Strategy +1. Test updating a message with valid data as the message owner +2. Test updating a message with valid data as an admin +3. Test updating a message with invalid data +4. Test updating a non-existent message +5. Test updating a message without permission +6. Test concurrent updates to the same message + +## Security Considerations +- Ensure proper authentication and authorization +- Validate input data to prevent injection attacks +- Ensure database queries are properly parameterized diff --git a/api/src/routes/rest/routes/messages/mod.rs b/api/src/routes/rest/routes/messages/mod.rs index 5afeba826..0ba6133ff 100644 --- a/api/src/routes/rest/routes/messages/mod.rs +++ b/api/src/routes/rest/routes/messages/mod.rs @@ -1,15 +1,19 @@ use axum::{ - routing::delete, + routing::{delete, put}, Router, }; mod delete_message; +mod update_message; use delete_message::delete_message_rest_handler; +use update_message::update_message_rest_handler; /// Create a router for message-related endpoints pub fn router() -> Router { Router::new() // Delete a message and all subsequent messages in the same chat .route("/:id", delete(delete_message_rest_handler)) -} \ No newline at end of file + // Update a message + .route("/:id", put(update_message_rest_handler)) +} \ No newline at end of file diff --git a/api/src/routes/rest/routes/messages/update_message.rs b/api/src/routes/rest/routes/messages/update_message.rs new file mode 100644 index 000000000..ac52cbf1c --- /dev/null +++ b/api/src/routes/rest/routes/messages/update_message.rs @@ -0,0 +1,48 @@ +use axum::{ + extract::{Json, Path}, + http::StatusCode, + Extension, +}; +use handlers::messages::update_message_handler; +use middleware::AuthenticatedUser; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::routes::rest::ApiResponse; + +/// Request body for updating a message +#[derive(Debug, Deserialize)] +pub struct UpdateMessageRequest { + /// Optional feedback for the message ("positive" or "negative") + pub feedback: Option, +} + +/// Update a specific message +/// +/// This endpoint allows updating properties of a message, such as adding feedback. +pub async fn update_message_rest_handler( + Extension(user): Extension, + Path(message_id): Path, + Json(request): Json, +) -> Result, (StatusCode, &'static str)> { + match update_message_handler(user, message_id, request.feedback).await { + Ok(_) => Ok(ApiResponse::NoContent), + Err(e) => { + tracing::error!("Error updating message: {}", e); + + let error_message = e.to_string(); + if error_message.contains("Message not found") { + Err((StatusCode::NOT_FOUND, "Message not found")) + } else if error_message.contains("don't have permission") { + Err((StatusCode::FORBIDDEN, "You don't have permission to update this message")) + } else if error_message.contains("must be either") { + Err((StatusCode::BAD_REQUEST, "Feedback must be either 'positive' or 'negative'")) + } else { + Err(( + StatusCode::INTERNAL_SERVER_ERROR, + "Failed to update message", + )) + } + } + } +}