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}