api/routes/modules/assignments/tickets/ticket_messages/delete.rs
1//! Ticket message deletion handler.
2//!
3//! Provides an endpoint to delete an existing message within a ticket.
4//!
5//! Only the author of the message can delete it. The endpoint validates
6//! that the user is the author before performing the deletion.
7
8use axum::{
9 Extension, Json,
10 extract::{Path, State},
11 http::StatusCode,
12 response::IntoResponse,
13};
14use db::models::ticket_messages::Model as TicketMessageModel;
15use util::state::AppState;
16
17use crate::{auth::AuthUser, response::ApiResponse, ws::tickets::topics::ticket_chat_topic};
18
19/// DELETE /api/modules/{module_id}/assignments/{assignment_id}/tickets/{ticket_id}/messages/{message_id}
20///
21/// Delete a **ticket message**. Only the **author** of the message may delete it.
22///
23/// ### Path Parameters
24/// - `module_id` (i64): Module ID (present in the route for authorization scope)
25/// - `assignment_id` (i64): Assignment ID (present in the route for authorization scope)
26/// - `ticket_id` (i64): Ticket ID (present in the route for authorization scope)
27/// - `message_id` (i64): The ID of the message to delete
28///
29/// ### Authorization
30/// - Requires a valid bearer token
31/// - Caller must be the **author** of the message; otherwise `403 Forbidden` is returned
32///
33/// ### WebSocket Broadcast
34/// - On success, broadcasts:
35/// ```json
36/// { "event": "message_deleted", "payload": { "id": <message_id> } }
37/// ```
38/// to topic:
39/// `ws/tickets/{ticket_id}`
40///
41/// ### Responses
42///
43/// - `200 OK` — Message deleted
44/// ```json
45/// {
46/// "success": true,
47/// "message": "Message deleted successfully",
48/// "data": { "id": 123 }
49/// }
50/// ```
51///
52/// - `403 Forbidden` — Caller is not the author
53/// ```json
54/// { "success": false, "message": "Forbidden" }
55/// ```
56///
57/// - `500 Internal Server Error` — Database error while deleting
58/// ```json
59/// { "success": false, "message": "Failed to delete message" }
60/// ```
61pub async fn delete_ticket_message(
62 // Capture all ids so we can build the WS topic
63 Path((_, _, ticket_id, message_id)): Path<(i64, i64, i64, i64)>,
64 State(app_state): State<AppState>,
65 Extension(AuthUser(claims)): Extension<AuthUser>,
66) -> impl IntoResponse {
67 let db = app_state.db();
68 let user_id = claims.sub;
69
70 // Author check
71 let is_author = TicketMessageModel::is_author(message_id, user_id, db).await;
72 if !is_author {
73 return (
74 StatusCode::FORBIDDEN,
75 Json(ApiResponse::<()>::error("Forbidden")),
76 )
77 .into_response();
78 }
79
80 // Delete
81 if let Err(_) = TicketMessageModel::delete(db, message_id).await {
82 return (
83 StatusCode::INTERNAL_SERVER_ERROR,
84 Json(ApiResponse::<()>::error("Failed to delete message")),
85 )
86 .into_response();
87 }
88
89 // Broadcast deletion to the per-ticket chat topic
90 let topic = ticket_chat_topic(ticket_id);
91 let ws = app_state.ws_clone();
92 let event = serde_json::json!({
93 "event": "message_deleted",
94 "payload": { "id": message_id }
95 });
96 ws.broadcast(&topic, event.to_string()).await;
97
98 // HTTP response
99 (
100 StatusCode::OK,
101 Json(ApiResponse::success(
102 serde_json::json!({ "id": message_id }),
103 "Message deleted successfully",
104 )),
105 )
106 .into_response()
107}