api/routes/modules/assignments/tickets/
put.rs

1//! Ticket status handlers.
2//!
3//! Provides endpoints to open or close a ticket in a module.
4//!
5//! Access control is enforced via the `is_valid` function, which ensures the
6//! user has permission to modify the ticket. Only the ticket owner or
7//! authorized users can perform these actions.
8
9use crate::{
10    auth::AuthUser,
11    response::ApiResponse,
12    routes::modules::assignments::tickets::common::is_valid,
13};
14use axum::{
15    Extension,
16    extract::{Path, State},
17    http::StatusCode,
18    response::{IntoResponse, Json},
19};
20use db::models::tickets::Model as TicketModel;
21use serde::Serialize;
22use util::state::AppState;
23
24/// Response payload for ticket status updates.
25#[derive(Serialize)]
26struct TicketStatusResponse {
27    /// Ticket ID
28    id: i64,
29    /// Current status of the ticket ("open" or "closed")
30    status: &'static str,
31}
32
33/// Opens a ticket.
34///
35/// **Endpoint:** `PUT /modules/{module_id}/assignments/{assignment_id}/tickets/{ticket_id}/open`  
36/// **Permissions:** Must be the ticket owner or an authorized user.
37///
38/// ### Path parameters
39/// - `module_id`       → ID of the module containing the ticket
40/// - `assignment_id`   → ID of the assignment (unused in this handler, kept for path consistency)
41/// - `ticket_id`       → ID of the ticket to open
42///
43/// ### Responses
44/// - `200 OK` → Ticket opened successfully
45/// ```json
46/// {
47///   "success": true,
48///   "data": { "id": 123, "status": "open" },
49///   "message": "Ticket opened successfully"
50/// }
51/// ```
52/// - `403 Forbidden` → User is not authorized to open this ticket
53/// ```json
54/// {
55///   "success": false,
56///   "data": null,
57///   "message": "User is not authorized to open this ticket"
58/// }
59/// ```
60/// - `500 Internal Server Error` → Failed to update ticket status
61/// ```json
62/// {
63///   "success": false,
64///   "data": null,
65///   "message": "Failed to open ticket"
66/// }
67/// ```
68
69pub async fn open_ticket(
70    State(app_state): State<AppState>,
71    Path((module_id, _, ticket_id)): Path<(i64, i64, i64)>,
72    Extension(AuthUser(claims)): Extension<AuthUser>,
73) -> impl IntoResponse {
74    let db = app_state.db();
75    let user_id = claims.sub;
76
77    if !is_valid(user_id, ticket_id, module_id, claims.admin, db).await {
78        return (
79            StatusCode::FORBIDDEN,
80            Json(ApiResponse::<()>::error("Forbidden")),
81        )
82            .into_response();
83    }
84
85    let data = TicketStatusResponse {
86        id: ticket_id,
87        status: "open",
88    };
89
90    match TicketModel::set_open(db, ticket_id).await {
91        Ok(_) => (
92            StatusCode::OK,
93            Json(ApiResponse::<TicketStatusResponse>::success(
94                data,
95                "Ticket opened successfully",
96            )),
97        )
98            .into_response(),
99        Err(_) => (
100            StatusCode::INTERNAL_SERVER_ERROR,
101            Json(ApiResponse::<()>::error("Failed to open ticket")),
102        )
103            .into_response(),
104    }
105}
106
107/// Closes a ticket.
108///
109/// **Endpoint:** `PUT /modules/{module_id}/assignments/{assignment_id}/tickets/{ticket_id}/close`  
110/// **Permissions:** Must be the ticket owner or an authorized user.
111///
112/// ### Path parameters
113/// - `module_id`       → ID of the module containing the ticket
114/// - `assignment_id`   → ID of the assignment (unused in this handler, kept for path consistency)
115/// - `ticket_id`       → ID of the ticket to close
116///
117/// ### Responses
118/// - `200 OK` → Ticket closed successfully
119/// ```json
120/// {
121///   "success": true,
122///   "data": { "id": 123, "status": "closed" },
123///   "message": "Ticket closed successfully"
124/// }
125/// ```
126/// - `403 Forbidden` → User is not authorized to close this ticket
127/// ```json
128/// {
129///   "success": false,
130///   "data": null,
131///   "message": "User is not authorized to close this ticket"
132/// }
133/// ```
134/// - `500 Internal Server Error` → Failed to update ticket status
135/// ```json
136/// {
137///   "success": false,
138///   "data": null,
139///   "message": "Failed to close ticket"
140/// }
141/// ```
142
143pub async fn close_ticket(
144    State(app_state): State<AppState>,
145    Path((module_id, _, ticket_id)): Path<(i64, i64, i64)>,
146    Extension(AuthUser(claims)): Extension<AuthUser>,
147) -> impl IntoResponse {
148    let db = app_state.db();
149    let user_id = claims.sub;
150
151    if !is_valid(user_id, ticket_id, module_id, claims.admin, db).await {
152        return (
153            StatusCode::FORBIDDEN,
154            Json(ApiResponse::<()>::error("Forbidden")),
155        )
156            .into_response();
157    }
158
159    let data = TicketStatusResponse {
160        id: ticket_id,
161        status: "closed",
162    };
163
164    match TicketModel::set_closed(db, ticket_id).await {
165        Ok(_) => (
166            StatusCode::OK,
167            Json(ApiResponse::<TicketStatusResponse>::success(
168                data,
169                "Ticket closed successfully",
170            )),
171        )
172            .into_response(),
173        Err(_) => (
174            StatusCode::INTERNAL_SERVER_ERROR,
175            Json(ApiResponse::<()>::error("Failed to close ticket")),
176        )
177            .into_response(),
178    }
179}