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}