1use std::collections::HashMap;
8use std::sync::Arc;
9
10use axum::Router;
11use serde::{Deserialize, Serialize};
12use uuid::Uuid;
13use chrono::{DateTime, Utc};
14
15use backbone_core::http::{ApiResponse, BackboneCrudHandler};
17
18#[cfg(feature = "auth")]
20use backbone_auth::middleware::AuthContext;
21#[cfg(feature = "auth")]
22use backbone_auth::AuthMiddleware;
23
24use crate::domain::entity::*;
26use crate::application::service::{StoredFileService, ServiceError};
27
28use crate::presentation::dto::{CreateStoredFileDto, UpdateStoredFileDto, PatchStoredFileDto, StoredFileResponseDto};
30
31use crate::domain::state_machine::{StoredFileState, StoredFileStateMachine, StoredFileTransition};
32
33#[derive(Debug, thiserror::Error)]
35pub enum StoredFileError {
36 #[error("Not found: {0}")]
37 NotFound(String),
38 #[error("Validation error: {0}")]
39 Validation(String),
40 #[error("Database error: {0}")]
41 Database(String),
42 #[error("Internal error: {0}")]
43 Internal(String),
44 #[error("File path too long (max 1024 characters): {0}")]
46 PathTooLong(String),
47 #[error("Path traversal not allowed: {0}")]
48 PathTraversal(String),
49 #[error("File name too long (max 255 characters): {0}")]
50 FilenameTooLong(String),
51 #[error("Invalid MIME type format: {0}")]
52 InvalidMimeType(String),
53 #[error("Cannot update purged files: {0}")]
54 FilePurged(String),
55 #[error("Only admins can modify quarantined files: {0}")]
56 QuarantineRestricted(String),
57}
58
59impl From<ServiceError> for StoredFileError {
60 fn from(err: ServiceError) -> Self {
61 match err {
62 ServiceError::NotFound => Self::NotFound(err.to_string()),
63 ServiceError::Validation(ref msg) => Self::Validation(msg.clone()),
64 ServiceError::AlreadyExists(ref msg) => Self::Validation(msg.clone()),
65 ServiceError::Repository(ref e) => Self::Database(e.to_string()),
66 ServiceError::Internal(ref msg) => Self::Internal(msg.clone()),
67 ServiceError::Violations(_) => Self::Validation(err.to_string()),
68 }
69 }
70}
71
72impl axum::response::IntoResponse for StoredFileError {
73 fn into_response(self) -> axum::response::Response {
74 use axum::http::StatusCode;
75 use axum::Json;
76
77 let (status, code) = match &self {
78 Self::NotFound(_) => (StatusCode::NOT_FOUND, "STOREDFILE_NOT_FOUND"),
79 Self::Validation(_) => (StatusCode::BAD_REQUEST, "STOREDFILE_VALIDATION_ERROR"),
80 Self::Database(_) => (StatusCode::INTERNAL_SERVER_ERROR, "STOREDFILE_DATABASE_ERROR"),
81 Self::Internal(_) => (StatusCode::INTERNAL_SERVER_ERROR, "STOREDFILE_INTERNAL_ERROR"),
82 Self::PathTooLong(_) => (StatusCode::UNPROCESSABLE_ENTITY, "STOREDFILE_PATH_TOO_LONG"),
83 Self::PathTraversal(_) => (StatusCode::UNPROCESSABLE_ENTITY, "STOREDFILE_PATH_TRAVERSAL"),
84 Self::FilenameTooLong(_) => (StatusCode::UNPROCESSABLE_ENTITY, "STOREDFILE_FILENAME_TOO_LONG"),
85 Self::InvalidMimeType(_) => (StatusCode::UNPROCESSABLE_ENTITY, "STOREDFILE_INVALID_MIME_TYPE"),
86 Self::FilePurged(_) => (StatusCode::UNPROCESSABLE_ENTITY, "STOREDFILE_FILE_PURGED"),
87 Self::QuarantineRestricted(_) => (StatusCode::UNPROCESSABLE_ENTITY, "STOREDFILE_QUARANTINE_RESTRICTED"),
88 };
89
90 let body = serde_json::json!({
91 "success": false,
92 "error": code,
93 "message": self.to_string(),
94 });
95
96 (status, Json(body)).into_response()
97 }
98}
99
100pub mod stored_file_errors {
102 pub const PATH_TOO_LONG: &str = "STOREDFILE_PATH_TOO_LONG";
103 pub const PATH_TRAVERSAL: &str = "STOREDFILE_PATH_TRAVERSAL";
104 pub const FILENAME_TOO_LONG: &str = "STOREDFILE_FILENAME_TOO_LONG";
105 pub const INVALID_MIME_TYPE: &str = "STOREDFILE_INVALID_MIME_TYPE";
106 pub const FILE_PURGED: &str = "STOREDFILE_FILE_PURGED";
107 pub const QUARANTINE_RESTRICTED: &str = "STOREDFILE_QUARANTINE_RESTRICTED";
108}
109
110pub fn create_stored_file_routes(service: Arc<StoredFileService>) -> Router {
143 BackboneCrudHandler::<StoredFileService, StoredFile, CreateStoredFileDto, UpdateStoredFileDto, StoredFileResponseDto>::routes(
144 service,
145 "/stored_files",
146 )
147}
148
149pub fn create_stored_file_read_routes(service: Arc<StoredFileService>) -> Router {
155 BackboneCrudHandler::<StoredFileService, StoredFile, CreateStoredFileDto, UpdateStoredFileDto, StoredFileResponseDto>::read_routes(
156 service,
157 "/stored_files",
158 )
159}
160
161pub fn create_stored_file_write_routes(service: Arc<StoredFileService>) -> Router {
173 BackboneCrudHandler::<StoredFileService, StoredFile, CreateStoredFileDto, UpdateStoredFileDto, StoredFileResponseDto>::write_routes(
174 service,
175 "/stored_files",
176 )
177}
178
179#[cfg(feature = "auth")]
185pub fn create_protected_stored_file_routes<A: AuthMiddleware + Send + Sync + 'static>(
186 service: Arc<StoredFileService>,
187 auth: Arc<A>,
188) -> Router {
189 use axum::middleware;
190 use axum::response::IntoResponse;
191
192 let auth_layer = auth.clone();
193 create_stored_file_routes(service)
194 .layer(middleware::from_fn(move |mut req: axum::extract::Request, next: axum::middleware::Next| {
195 let auth = auth_layer.clone();
196 async move {
197 let token = req.headers()
198 .get(axum::http::header::AUTHORIZATION)
199 .and_then(|h| h.to_str().ok())
200 .and_then(|raw| raw.strip_prefix("Bearer ").or_else(|| raw.strip_prefix("bearer ")))
201 .unwrap_or("");
202 match auth.authenticate(token).await {
203 Ok(ctx) => {
204 req.extensions_mut().insert(ctx);
205 next.run(req).await
206 }
207 Err(_) => {
208 (axum::http::StatusCode::UNAUTHORIZED,
209 axum::Json(serde_json::json!({
210 "success": false,
211 "error": "unauthorized",
212 "message": "Authentication required"
213 }))
214 ).into_response()
215 }
216 }
217 }
218 }))
219}
220
221pub async fn complete_upload_transition(
229 axum::extract::State(service): axum::extract::State<Arc<StoredFileService>>,
230 axum::extract::Path(id): axum::extract::Path<String>,
231 #[cfg(feature = "auth")] axum::Extension(auth): axum::Extension<AuthContext>,
232) -> impl axum::response::IntoResponse {
233 use axum::{http::StatusCode, Json};
234
235 let entity = match service.get_by_id(&id).await {
237 Ok(Some(e)) => e,
238 Ok(None) => return (StatusCode::NOT_FOUND, Json(ApiResponse::<StoredFileResponseDto>::not_found("StoredFile", &id))),
239 Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(ApiResponse::<StoredFileResponseDto>::error(e.to_string()))),
240 };
241
242 #[cfg(feature = "auth")]
244 {
245 let allowed_roles = StoredFileTransition::CompleteUpload.allowed_roles();
246 let has_specific_perm = auth.permissions.iter().any(|p| p == "stored_file:transition:complete_upload");
247 let has_update_perm = auth.permissions.iter().any(|p| p == "stored_file:update");
248 let has_role = auth.roles.iter().any(|r| allowed_roles.contains(&r.as_str()));
249 if !has_specific_perm && !has_update_perm && !has_role {
250 return (StatusCode::FORBIDDEN, Json(ApiResponse::<StoredFileResponseDto>::error("Insufficient permissions for complete_upload transition")));
251 }
252 }
253
254 let current_state: StoredFileState = entity.status.to_string().parse()
256 .unwrap_or(StoredFileState::default());
257 let sm = StoredFileStateMachine::from_state(current_state);
258 if !sm.can_transition(StoredFileTransition::CompleteUpload) {
259 return (StatusCode::BAD_REQUEST, Json(ApiResponse::<StoredFileResponseDto>::error("Transition not allowed from current state")));
260 }
261
262 let mut fields: HashMap<String, serde_json::Value> = HashMap::new();
264 fields.insert("status".to_string(), serde_json::Value::String("Processing".to_string()));
265
266 match service.partial_update(&id, fields).await {
267 Ok(Some(updated)) => {
268 let response: StoredFileResponseDto = updated.into();
269 (StatusCode::OK, Json(ApiResponse::ok(response)))
270 }
271 Ok(None) => (StatusCode::NOT_FOUND, Json(ApiResponse::<StoredFileResponseDto>::not_found("StoredFile", &id))),
272 Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(ApiResponse::<StoredFileResponseDto>::error(e.to_string()))),
273 }
274}
275
276pub async fn mark_safe_transition(
280 axum::extract::State(service): axum::extract::State<Arc<StoredFileService>>,
281 axum::extract::Path(id): axum::extract::Path<String>,
282 #[cfg(feature = "auth")] axum::Extension(auth): axum::Extension<AuthContext>,
283) -> impl axum::response::IntoResponse {
284 use axum::{http::StatusCode, Json};
285
286 let entity = match service.get_by_id(&id).await {
288 Ok(Some(e)) => e,
289 Ok(None) => return (StatusCode::NOT_FOUND, Json(ApiResponse::<StoredFileResponseDto>::not_found("StoredFile", &id))),
290 Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(ApiResponse::<StoredFileResponseDto>::error(e.to_string()))),
291 };
292
293 #[cfg(feature = "auth")]
295 {
296 let allowed_roles = StoredFileTransition::MarkSafe.allowed_roles();
297 let has_specific_perm = auth.permissions.iter().any(|p| p == "stored_file:transition:mark_safe");
298 let has_update_perm = auth.permissions.iter().any(|p| p == "stored_file:update");
299 let has_role = auth.roles.iter().any(|r| allowed_roles.contains(&r.as_str()));
300 if !has_specific_perm && !has_update_perm && !has_role {
301 return (StatusCode::FORBIDDEN, Json(ApiResponse::<StoredFileResponseDto>::error("Insufficient permissions for mark_safe transition")));
302 }
303 }
304
305 let current_state: StoredFileState = entity.status.to_string().parse()
307 .unwrap_or(StoredFileState::default());
308 let sm = StoredFileStateMachine::from_state(current_state);
309 if !sm.can_transition(StoredFileTransition::MarkSafe) {
310 return (StatusCode::BAD_REQUEST, Json(ApiResponse::<StoredFileResponseDto>::error("Transition not allowed from current state")));
311 }
312
313 let mut fields: HashMap<String, serde_json::Value> = HashMap::new();
315 fields.insert("status".to_string(), serde_json::Value::String("Active".to_string()));
316
317 match service.partial_update(&id, fields).await {
318 Ok(Some(updated)) => {
319 let response: StoredFileResponseDto = updated.into();
320 (StatusCode::OK, Json(ApiResponse::ok(response)))
321 }
322 Ok(None) => (StatusCode::NOT_FOUND, Json(ApiResponse::<StoredFileResponseDto>::not_found("StoredFile", &id))),
323 Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(ApiResponse::<StoredFileResponseDto>::error(e.to_string()))),
324 }
325}
326
327pub async fn quarantine_transition(
331 axum::extract::State(service): axum::extract::State<Arc<StoredFileService>>,
332 axum::extract::Path(id): axum::extract::Path<String>,
333 #[cfg(feature = "auth")] axum::Extension(auth): axum::Extension<AuthContext>,
334) -> impl axum::response::IntoResponse {
335 use axum::{http::StatusCode, Json};
336
337 let entity = match service.get_by_id(&id).await {
339 Ok(Some(e)) => e,
340 Ok(None) => return (StatusCode::NOT_FOUND, Json(ApiResponse::<StoredFileResponseDto>::not_found("StoredFile", &id))),
341 Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(ApiResponse::<StoredFileResponseDto>::error(e.to_string()))),
342 };
343
344 #[cfg(feature = "auth")]
346 {
347 let allowed_roles = StoredFileTransition::Quarantine.allowed_roles();
348 let has_specific_perm = auth.permissions.iter().any(|p| p == "stored_file:transition:quarantine");
349 let has_update_perm = auth.permissions.iter().any(|p| p == "stored_file:update");
350 let has_role = auth.roles.iter().any(|r| allowed_roles.contains(&r.as_str()));
351 if !has_specific_perm && !has_update_perm && !has_role {
352 return (StatusCode::FORBIDDEN, Json(ApiResponse::<StoredFileResponseDto>::error("Insufficient permissions for quarantine transition")));
353 }
354 }
355
356 let current_state: StoredFileState = entity.status.to_string().parse()
358 .unwrap_or(StoredFileState::default());
359 let sm = StoredFileStateMachine::from_state(current_state);
360 if !sm.can_transition(StoredFileTransition::Quarantine) {
361 return (StatusCode::BAD_REQUEST, Json(ApiResponse::<StoredFileResponseDto>::error("Transition not allowed from current state")));
362 }
363
364 let mut fields: HashMap<String, serde_json::Value> = HashMap::new();
366 fields.insert("status".to_string(), serde_json::Value::String("Quarantined".to_string()));
367
368 match service.partial_update(&id, fields).await {
369 Ok(Some(updated)) => {
370 let response: StoredFileResponseDto = updated.into();
371 (StatusCode::OK, Json(ApiResponse::ok(response)))
372 }
373 Ok(None) => (StatusCode::NOT_FOUND, Json(ApiResponse::<StoredFileResponseDto>::not_found("StoredFile", &id))),
374 Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(ApiResponse::<StoredFileResponseDto>::error(e.to_string()))),
375 }
376}
377
378pub async fn mark_safe_by_admin_transition(
382 axum::extract::State(service): axum::extract::State<Arc<StoredFileService>>,
383 axum::extract::Path(id): axum::extract::Path<String>,
384 #[cfg(feature = "auth")] axum::Extension(auth): axum::Extension<AuthContext>,
385) -> impl axum::response::IntoResponse {
386 use axum::{http::StatusCode, Json};
387
388 let entity = match service.get_by_id(&id).await {
390 Ok(Some(e)) => e,
391 Ok(None) => return (StatusCode::NOT_FOUND, Json(ApiResponse::<StoredFileResponseDto>::not_found("StoredFile", &id))),
392 Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(ApiResponse::<StoredFileResponseDto>::error(e.to_string()))),
393 };
394
395 #[cfg(feature = "auth")]
397 {
398 let allowed_roles = StoredFileTransition::MarkSafeByAdmin.allowed_roles();
399 let has_specific_perm = auth.permissions.iter().any(|p| p == "stored_file:transition:mark_safe_by_admin");
400 let has_update_perm = auth.permissions.iter().any(|p| p == "stored_file:update");
401 let has_role = auth.roles.iter().any(|r| allowed_roles.contains(&r.as_str()));
402 if !has_specific_perm && !has_update_perm && !has_role {
403 return (StatusCode::FORBIDDEN, Json(ApiResponse::<StoredFileResponseDto>::error("Insufficient permissions for mark_safe_by_admin transition")));
404 }
405 }
406
407 let current_state: StoredFileState = entity.status.to_string().parse()
409 .unwrap_or(StoredFileState::default());
410 let sm = StoredFileStateMachine::from_state(current_state);
411 if !sm.can_transition(StoredFileTransition::MarkSafeByAdmin) {
412 return (StatusCode::BAD_REQUEST, Json(ApiResponse::<StoredFileResponseDto>::error("Transition not allowed from current state")));
413 }
414
415 let mut fields: HashMap<String, serde_json::Value> = HashMap::new();
417 fields.insert("status".to_string(), serde_json::Value::String("Active".to_string()));
418
419 match service.partial_update(&id, fields).await {
420 Ok(Some(updated)) => {
421 let response: StoredFileResponseDto = updated.into();
422 (StatusCode::OK, Json(ApiResponse::ok(response)))
423 }
424 Ok(None) => (StatusCode::NOT_FOUND, Json(ApiResponse::<StoredFileResponseDto>::not_found("StoredFile", &id))),
425 Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(ApiResponse::<StoredFileResponseDto>::error(e.to_string()))),
426 }
427}
428
429pub async fn soft_delete_transition(
433 axum::extract::State(service): axum::extract::State<Arc<StoredFileService>>,
434 axum::extract::Path(id): axum::extract::Path<String>,
435 #[cfg(feature = "auth")] axum::Extension(auth): axum::Extension<AuthContext>,
436) -> impl axum::response::IntoResponse {
437 use axum::{http::StatusCode, Json};
438
439 let entity = match service.get_by_id(&id).await {
441 Ok(Some(e)) => e,
442 Ok(None) => return (StatusCode::NOT_FOUND, Json(ApiResponse::<StoredFileResponseDto>::not_found("StoredFile", &id))),
443 Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(ApiResponse::<StoredFileResponseDto>::error(e.to_string()))),
444 };
445
446 #[cfg(feature = "auth")]
448 {
449 let allowed_roles = StoredFileTransition::SoftDelete.allowed_roles();
450 let has_specific_perm = auth.permissions.iter().any(|p| p == "stored_file:transition:soft_delete");
451 let has_update_perm = auth.permissions.iter().any(|p| p == "stored_file:update");
452 let has_role = auth.roles.iter().any(|r| allowed_roles.contains(&r.as_str()));
453 if !has_specific_perm && !has_update_perm && !has_role {
454 return (StatusCode::FORBIDDEN, Json(ApiResponse::<StoredFileResponseDto>::error("Insufficient permissions for soft_delete transition")));
455 }
456 }
457
458 let current_state: StoredFileState = entity.status.to_string().parse()
460 .unwrap_or(StoredFileState::default());
461 let sm = StoredFileStateMachine::from_state(current_state);
462 if !sm.can_transition(StoredFileTransition::SoftDelete) {
463 return (StatusCode::BAD_REQUEST, Json(ApiResponse::<StoredFileResponseDto>::error("Transition not allowed from current state")));
464 }
465
466 let mut fields: HashMap<String, serde_json::Value> = HashMap::new();
468 fields.insert("status".to_string(), serde_json::Value::String("Deleted".to_string()));
469
470 match service.partial_update(&id, fields).await {
471 Ok(Some(updated)) => {
472 let response: StoredFileResponseDto = updated.into();
473 (StatusCode::OK, Json(ApiResponse::ok(response)))
474 }
475 Ok(None) => (StatusCode::NOT_FOUND, Json(ApiResponse::<StoredFileResponseDto>::not_found("StoredFile", &id))),
476 Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(ApiResponse::<StoredFileResponseDto>::error(e.to_string()))),
477 }
478}
479
480pub async fn restore_transition(
484 axum::extract::State(service): axum::extract::State<Arc<StoredFileService>>,
485 axum::extract::Path(id): axum::extract::Path<String>,
486 #[cfg(feature = "auth")] axum::Extension(auth): axum::Extension<AuthContext>,
487) -> impl axum::response::IntoResponse {
488 use axum::{http::StatusCode, Json};
489
490 let entity = match service.get_by_id(&id).await {
492 Ok(Some(e)) => e,
493 Ok(None) => return (StatusCode::NOT_FOUND, Json(ApiResponse::<StoredFileResponseDto>::not_found("StoredFile", &id))),
494 Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(ApiResponse::<StoredFileResponseDto>::error(e.to_string()))),
495 };
496
497 #[cfg(feature = "auth")]
499 {
500 let allowed_roles = StoredFileTransition::Restore.allowed_roles();
501 let has_specific_perm = auth.permissions.iter().any(|p| p == "stored_file:transition:restore");
502 let has_update_perm = auth.permissions.iter().any(|p| p == "stored_file:update");
503 let has_role = auth.roles.iter().any(|r| allowed_roles.contains(&r.as_str()));
504 if !has_specific_perm && !has_update_perm && !has_role {
505 return (StatusCode::FORBIDDEN, Json(ApiResponse::<StoredFileResponseDto>::error("Insufficient permissions for restore transition")));
506 }
507 }
508
509 let current_state: StoredFileState = entity.status.to_string().parse()
511 .unwrap_or(StoredFileState::default());
512 let sm = StoredFileStateMachine::from_state(current_state);
513 if !sm.can_transition(StoredFileTransition::Restore) {
514 return (StatusCode::BAD_REQUEST, Json(ApiResponse::<StoredFileResponseDto>::error("Transition not allowed from current state")));
515 }
516
517 let mut fields: HashMap<String, serde_json::Value> = HashMap::new();
519 fields.insert("status".to_string(), serde_json::Value::String("Active".to_string()));
520
521 match service.partial_update(&id, fields).await {
522 Ok(Some(updated)) => {
523 let response: StoredFileResponseDto = updated.into();
524 (StatusCode::OK, Json(ApiResponse::ok(response)))
525 }
526 Ok(None) => (StatusCode::NOT_FOUND, Json(ApiResponse::<StoredFileResponseDto>::not_found("StoredFile", &id))),
527 Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(ApiResponse::<StoredFileResponseDto>::error(e.to_string()))),
528 }
529}
530
531pub async fn purge_transition(
535 axum::extract::State(service): axum::extract::State<Arc<StoredFileService>>,
536 axum::extract::Path(id): axum::extract::Path<String>,
537 #[cfg(feature = "auth")] axum::Extension(auth): axum::Extension<AuthContext>,
538) -> impl axum::response::IntoResponse {
539 use axum::{http::StatusCode, Json};
540
541 let entity = match service.get_by_id(&id).await {
543 Ok(Some(e)) => e,
544 Ok(None) => return (StatusCode::NOT_FOUND, Json(ApiResponse::<StoredFileResponseDto>::not_found("StoredFile", &id))),
545 Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(ApiResponse::<StoredFileResponseDto>::error(e.to_string()))),
546 };
547
548 #[cfg(feature = "auth")]
550 {
551 let allowed_roles = StoredFileTransition::Purge.allowed_roles();
552 let has_specific_perm = auth.permissions.iter().any(|p| p == "stored_file:transition:purge");
553 let has_update_perm = auth.permissions.iter().any(|p| p == "stored_file:update");
554 let has_role = auth.roles.iter().any(|r| allowed_roles.contains(&r.as_str()));
555 if !has_specific_perm && !has_update_perm && !has_role {
556 return (StatusCode::FORBIDDEN, Json(ApiResponse::<StoredFileResponseDto>::error("Insufficient permissions for purge transition")));
557 }
558 }
559
560 let current_state: StoredFileState = entity.status.to_string().parse()
562 .unwrap_or(StoredFileState::default());
563 let sm = StoredFileStateMachine::from_state(current_state);
564 if !sm.can_transition(StoredFileTransition::Purge) {
565 return (StatusCode::BAD_REQUEST, Json(ApiResponse::<StoredFileResponseDto>::error("Transition not allowed from current state")));
566 }
567
568 let mut fields: HashMap<String, serde_json::Value> = HashMap::new();
570 fields.insert("status".to_string(), serde_json::Value::String("Purged".to_string()));
571
572 match service.partial_update(&id, fields).await {
573 Ok(Some(updated)) => {
574 let response: StoredFileResponseDto = updated.into();
575 (StatusCode::OK, Json(ApiResponse::ok(response)))
576 }
577 Ok(None) => (StatusCode::NOT_FOUND, Json(ApiResponse::<StoredFileResponseDto>::not_found("StoredFile", &id))),
578 Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(ApiResponse::<StoredFileResponseDto>::error(e.to_string()))),
579 }
580}
581
582pub fn create_stored_file_transition_routes(service: Arc<StoredFileService>) -> Router {
584 use axum::routing::post;
585
586 Router::new()
587 .route("/stored_files/:id/transitions/complete_upload", post(complete_upload_transition))
588 .route("/stored_files/:id/transitions/mark_safe", post(mark_safe_transition))
589 .route("/stored_files/:id/transitions/quarantine", post(quarantine_transition))
590 .route("/stored_files/:id/transitions/mark_safe_by_admin", post(mark_safe_by_admin_transition))
591 .route("/stored_files/:id/transitions/soft_delete", post(soft_delete_transition))
592 .route("/stored_files/:id/transitions/restore", post(restore_transition))
593 .route("/stored_files/:id/transitions/purge", post(purge_transition))
594 .with_state(service)
595}