api/routes/modules/assignments/tickets/ticket_messages/
post.rs

1//! Ticket message creation handler.
2//!
3//! Provides an endpoint to create a new message for a ticket in a module.
4//!
5//! Only users authorized to view the ticket (author or staff) can create messages.
6
7use axum::{
8    Extension, Json,
9    extract::{Path, State},
10    http::StatusCode,
11    response::IntoResponse,
12};
13use db::models::{
14    ticket_messages::Model as TicketMessageModel,
15    user::{Column as UserColumn, Entity as UserEntity, Model as UserModel},
16};
17use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
18use util::state::AppState;
19
20use crate::{
21    auth::AuthUser,
22    response::ApiResponse,
23    routes::modules::assignments::tickets::{
24        common::is_valid,
25        ticket_messages::common::{MessageResponse, UserResponse},
26    }, ws::tickets::topics::ticket_chat_topic,
27};
28
29/// POST /api/modules/{module_id}/assignments/{assignment_id}/tickets/{ticket_id}/messages
30///
31/// Create a **new message** in a ticket and broadcast a WebSocket event on:
32/// `ws/tickets/{ticket_id}`
33///
34/// ### Path Parameters
35/// - `module_id` (i64)
36/// - `assignment_id` (i64)
37/// - `ticket_id` (i64)
38///
39/// ### Request Body (JSON)
40///
41/// { "content": "Can someone review my latest attempt?" }
42/// 
43/// ### Responses
44/// 
45/// - `200 OK` → Message created successfully
46/// ```json
47/// {
48///   "success": true,
49///   "data": {
50///       "id": 123,
51///       "ticket_id": 456,
52///       "content": "Message content here",
53///       "created_at": "2025-08-18T10:00:00Z",
54///       "updated_at": "2025-08-18T10:00:00Z",
55///       "user": { "id": 789, "username": "john_doe" }
56///   },
57///   "message": "Message created successfully"
58/// }
59/// ```
60/// - `400 Bad Request` → Content missing or empty
61/// ```json
62/// {
63///   "success": false,
64///   "data": null,
65///   "message": "Content is required"
66/// }
67/// ```
68/// - `403 Forbidden` → User not authorized to create a message for this ticket
69/// ```json
70/// {
71///   "success": false,
72///   "data": null,
73///   "message": "Forbidden"
74/// }
75/// ```
76/// - `404 Not Found` → User not found
77/// ```json
78/// {
79///   "success": false,
80///   "data": null,
81///   "message": "User not found"
82/// }
83/// ```
84/// - `500 Internal Server Error` → Failed to create the message
85/// ```json
86/// {
87///   "success": false,
88///   "data": null,
89///   "message": "Failed to create message"
90/// }
91/// ```
92pub async fn create_message(
93    Path((module_id, _, ticket_id)): Path<(i64, i64, i64)>,
94    State(app_state): State<AppState>,
95    Extension(AuthUser(claims)): Extension<AuthUser>,
96    Json(req): Json<serde_json::Value>,
97) -> impl IntoResponse {
98    let db = app_state.db();
99    let user_id = claims.sub;
100
101    if !is_valid(user_id, ticket_id, module_id, claims.admin, db).await {
102        return (
103            StatusCode::FORBIDDEN,
104            Json(ApiResponse::<()>::error("Forbidden")),
105        )
106            .into_response();
107    }
108
109    let content = match req.get("content").and_then(|v| v.as_str()) {
110        Some(c) if !c.trim().is_empty() => c.trim().to_string(),
111        _ => {
112            return (
113                StatusCode::BAD_REQUEST,
114                Json(ApiResponse::<()>::error("Content is required")),
115            )
116                .into_response();
117        }
118    };
119
120    let user: Option<UserModel> = UserEntity::find()
121        .filter(UserColumn::Id.eq(user_id))
122        .one(db)
123        .await
124        .unwrap_or(None);
125
126    let user = match user {
127        Some(u) => u,
128        None => {
129            return (
130                StatusCode::NOT_FOUND,
131                Json(ApiResponse::<()>::error("User not found")),
132            )
133                .into_response();
134        }
135    };
136
137    let message = match TicketMessageModel::create(db, ticket_id, user_id, &content).await {
138        Ok(msg) => msg,
139        Err(_) => {
140            return (
141                StatusCode::INTERNAL_SERVER_ERROR,
142                Json(ApiResponse::<()>::error("Failed to create message")),
143            )
144                .into_response();
145        }
146    };
147
148    let response = MessageResponse {
149        id: message.id,
150        ticket_id: message.ticket_id,
151        content: message.content,
152        created_at: message.created_at.to_rfc3339(),
153        updated_at: message.updated_at.to_rfc3339(),
154        user: Some(UserResponse {
155            id: user.id,
156            username: user.username,
157        }),
158    };
159
160    // ---- WebSocket broadcast: notify subscribers on this ticket's topic ----
161    // Topic: ws/tickets/{ticket_id}
162    let topic = ticket_chat_topic(ticket_id);
163    let ws = app_state.ws_clone();
164    let event_json = serde_json::json!({
165        "event": "message_created",
166        "payload": &response
167    });
168    ws.broadcast(&topic, event_json.to_string()).await;
169
170    (
171        StatusCode::OK,
172        Json(ApiResponse::success(
173            response,
174            "Message created successfully",
175        )),
176    )
177        .into_response()
178}