use std::sync::Arc; use uuid::Uuid; use axum::{ Json, extract::{Path, Query, State}, http::StatusCode, response::IntoResponse, }; use serde::Deserialize; use serde_json::json; use utoipa::ToSchema; use crate::application::services::share_service::ShareService; use crate::{ application::{ dtos::share_dto::{CreateShareDto, UpdateShareDto}, ports::share_ports::ShareUseCase, }, common::errors::ErrorKind, domain::entities::share::ShareItemType, interfaces::errors::AppError, interfaces::middleware::auth::AuthUser, }; #[derive(Debug, Deserialize)] pub struct GetSharesQuery { pub page: Option, pub per_page: Option, pub item_id: Option, pub item_type: Option, } #[derive(Debug, Deserialize, ToSchema)] pub struct VerifyPasswordRequest { pub password: String, } /// Create a new shared link #[utoipa::path( post, path = "/api/shares", request_body = CreateShareDto, responses( (status = 201, description = "Share created", body = crate::application::dtos::share_dto::ShareDto), (status = 400, description = "Bad request") ), tag = "shares" )] pub async fn create_shared_link( State(share_use_case): State>, auth_user: AuthUser, Json(dto): Json, ) -> impl IntoResponse { match share_use_case.create_shared_link(auth_user.id, dto).await { Ok(share) => (StatusCode::CREATED, Json(share)).into_response(), Err(err) => AppError::from(err).into_response(), } } /// Get information about a specific shared link by ID #[utoipa::path( get, path = "/api/shares/{id}", params(("id" = String, Path, description = "Share ID")), responses( (status = 200, description = "Share details", body = crate::application::dtos::share_dto::ShareDto), (status = 404, description = "Share not found") ), tag = "shares" )] pub async fn get_shared_link( State(share_use_case): State>, auth_user: AuthUser, Path(id): Path, ) -> impl IntoResponse { let id = match Uuid::parse_str(&id) { Ok(id) => id, Err(_) => return AppError::bad_request("Invalid UUID").into_response(), }; match share_use_case.get_shared_link(id, auth_user.id).await { Ok(share) => (StatusCode::OK, Json(share)).into_response(), Err(err) => AppError::from(err).into_response(), } } /// Get all shared links created by the current user. /// Supports optional filtering by item_id + item_type query params. #[utoipa::path( get, path = "/api/shares", responses( (status = 200, description = "List of shares", body = Vec) ), tag = "shares" )] pub async fn get_user_shares( State(share_use_case): State>, auth_user: AuthUser, Query(query): Query, ) -> impl IntoResponse { let user_id = auth_user.id; // If both item_id and item_type are provided, return shares for that specific item if let (Some(item_id), Some(item_type_str)) = (&query.item_id, &query.item_type) { let item_type = match ShareItemType::try_from(item_type_str.as_str()) { Ok(t) => t, Err(_) => { return ( StatusCode::BAD_REQUEST, Json(json!({ "error": format!("Invalid item_type: {}", item_type_str) })), ) .into_response(); } }; return match share_use_case .get_shared_links_for_item(item_id, &item_type, user_id) .await { Ok(shares) => (StatusCode::OK, Json(shares)).into_response(), Err(err) => AppError::from(err).into_response(), }; } // Default: paginated list of all user shares let page = query.page.unwrap_or(1); let per_page = query.per_page.unwrap_or(20); match share_use_case .get_user_shared_links(user_id, page, per_page) .await { Ok(shares) => (StatusCode::OK, Json(shares)).into_response(), Err(err) => AppError::from(err).into_response(), } } /// Update a shared link's properties #[utoipa::path( put, path = "/api/shares/{id}", params(("id" = String, Path, description = "Share ID")), request_body = UpdateShareDto, responses( (status = 200, description = "Share updated", body = crate::application::dtos::share_dto::ShareDto), (status = 404, description = "Share not found") ), tag = "shares" )] pub async fn update_shared_link( State(share_use_case): State>, auth_user: AuthUser, Path(id): Path, Json(dto): Json, ) -> impl IntoResponse { let id = match Uuid::parse_str(&id) { Ok(id) => id, Err(_) => return AppError::bad_request("Invalid UUID").into_response(), }; match share_use_case .update_shared_link(id, auth_user.id, dto) .await { Ok(share) => (StatusCode::OK, Json(share)).into_response(), Err(err) => AppError::from(err).into_response(), } } /// Delete a shared link #[utoipa::path( delete, path = "/api/shares/{id}", params(("id" = String, Path, description = "Share ID")), responses( (status = 204, description = "Share deleted"), (status = 404, description = "Share not found") ), tag = "shares" )] pub async fn delete_shared_link( State(share_use_case): State>, auth_user: AuthUser, Path(id): Path, ) -> impl IntoResponse { let id = match Uuid::parse_str(&id) { Ok(id) => id, Err(_) => return AppError::bad_request("Invalid UUID").into_response(), }; match share_use_case.delete_shared_link(id, auth_user.id).await { Ok(_) => StatusCode::NO_CONTENT.into_response(), Err(err) => AppError::from(err).into_response(), } } /// Access a shared item via its token #[utoipa::path( get, path = "/api/s/{token}", params(("token" = String, Path, description = "Share token")), responses( (status = 200, description = "Shared item details"), (status = 401, description = "Password required"), (status = 410, description = "Share expired") ), tag = "shares" )] pub async fn access_shared_item( State(share_use_case): State>, Path(token): Path, ) -> impl IntoResponse { // Register the access let _ = share_use_case.register_shared_link_access(&token).await; // Get the shared link match share_use_case.get_shared_link_by_token(&token).await { Ok(item) => (StatusCode::OK, Json(item)).into_response(), Err(err) => { // Special handling for share access errors if err.kind == ErrorKind::AccessDenied { if err.message.contains("password") { return ( StatusCode::UNAUTHORIZED, Json(json!({ "error": "Password required", "requiresPassword": true })), ) .into_response(); } if err.message.contains("expired") { return AppError::new(StatusCode::GONE, err.message, "Expired").into_response(); } } AppError::from(err).into_response() } } } /// Verify password for a password-protected shared item #[utoipa::path( post, path = "/api/s/{token}/verify", params(("token" = String, Path, description = "Share token")), responses( (status = 200, description = "Password verified, item details returned"), (status = 401, description = "Invalid password"), (status = 410, description = "Share expired") ), tag = "shares" )] pub async fn verify_shared_item_password( State(share_use_case): State>, Path(token): Path, Json(req): Json, ) -> impl IntoResponse { match share_use_case .verify_shared_link_password(&token, &req.password) .await { Ok(item) => (StatusCode::OK, Json(item)).into_response(), Err(err) => { if err.kind == ErrorKind::AccessDenied { if err.message.contains("expired") { return AppError::new(StatusCode::GONE, err.message, "Expired").into_response(); } if err.message.contains("password") { return AppError::unauthorized("Invalid password").into_response(); } } AppError::from(err).into_response() } } }