use axum::{ extract::{Path, State}, http::StatusCode, response::IntoResponse, Json, Extension, }; use serde::{Deserialize, Serialize}; use validator::Validate; use mongodb::bson::oid::ObjectId; use crate::{ auth::jwt::Claims, config::AppState, models::{share::Share, permission::Permission}, }; #[derive(Debug, Deserialize, Validate)] pub struct CreateShareRequest { pub target_user_email: String, pub resource_type: String, pub resource_id: Option, pub permissions: Vec, #[serde(default)] pub expires_days: Option, } #[derive(Debug, Serialize)] pub struct ShareResponse { pub id: String, pub target_user_id: String, pub resource_type: String, pub resource_id: Option, pub permissions: Vec, pub expires_at: Option, pub created_at: String, pub active: bool, } impl TryFrom for ShareResponse { type Error = anyhow::Error; fn try_from(share: Share) -> Result { Ok(Self { id: share.id.map(|id| id.to_string()).unwrap_or_default(), target_user_id: share.target_user_id.to_string(), resource_type: share.resource_type, resource_id: share.resource_id.map(|id| id.to_string()), permissions: share.permissions.into_iter().map(|p| p.to_string()).collect(), expires_at: share.expires_at.map(|dt| dt.to_rfc3339()), created_at: share.created_at.to_rfc3339(), active: share.active, }) } } pub async fn create_share( State(state): State, Extension(claims): Extension, Json(req): Json, ) -> impl IntoResponse { if let Err(errors) = req.validate() { return (StatusCode::BAD_REQUEST, Json(serde_json::json!({ "error": "validation failed", "details": errors.to_string() }))).into_response(); } // Find target user by email let target_user = match state.db.find_user_by_email(&req.target_user_email).await { Ok(Some(user)) => user, Ok(None) => { return (StatusCode::NOT_FOUND, Json(serde_json::json!({ "error": "target user not found" }))).into_response(); } Err(e) => { tracing::error!("Failed to find target user: {}", e); return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": "database error" }))).into_response(); } }; let target_user_id = match target_user.id { Some(id) => id, None => { return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": "target user has no ID" }))).into_response(); } }; let owner_id = match ObjectId::parse_str(&claims.sub) { Ok(id) => id, Err(_) => { return (StatusCode::BAD_REQUEST, Json(serde_json::json!({ "error": "invalid user ID format" }))).into_response(); } }; // Parse resource_id if provided let resource_id = match req.resource_id { Some(id) => { match ObjectId::parse_str(&id) { Ok(oid) => Some(oid), Err(_) => { return (StatusCode::BAD_REQUEST, Json(serde_json::json!({ "error": "invalid resource_id format" }))).into_response(); } } }, None => None, }; // Parse permissions - support all permission types let permissions: Vec = req.permissions .into_iter() .filter_map(|p| match p.to_lowercase().as_str() { "read" => Some(Permission::Read), "write" => Some(Permission::Write), "delete" => Some(Permission::Delete), "share" => Some(Permission::Share), "admin" => Some(Permission::Admin), _ => None, }) .collect(); if permissions.is_empty() { return (StatusCode::BAD_REQUEST, Json(serde_json::json!({ "error": "at least one valid permission is required (read, write, delete, share, admin)" }))).into_response(); } // Calculate expiration let expires_at = req.expires_days.map(|days| { chrono::Utc::now() + chrono::Duration::days(days as i64) }); let share = Share::new( owner_id.clone(), target_user_id, req.resource_type, resource_id, permissions, expires_at, ); match state.db.create_share(&share).await { Ok(_) => { let response: ShareResponse = match share.try_into() { Ok(r) => r, Err(_) => { return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": "failed to create share response" }))).into_response(); } }; (StatusCode::CREATED, Json(response)).into_response() } Err(e) => { tracing::error!("Failed to create share: {}", e); (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": "failed to create share" }))).into_response() } } } pub async fn list_shares( State(state): State, Extension(claims): Extension, ) -> impl IntoResponse { let user_id = &claims.sub; match state.db.list_shares_for_user(user_id).await { Ok(shares) => { let responses: Vec = shares .into_iter() .filter_map(|s| ShareResponse::try_from(s).ok()) .collect(); (StatusCode::OK, Json(responses)).into_response() } Err(e) => { tracing::error!("Failed to list shares: {}", e); (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": "failed to list shares" }))).into_response() } } } pub async fn get_share( State(state): State, Path(id): Path, ) -> impl IntoResponse { match state.db.get_share(&id).await { Ok(Some(share)) => { let response: ShareResponse = match share.try_into() { Ok(r) => r, Err(_) => { return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": "failed to create share response" }))).into_response(); } }; (StatusCode::OK, Json(response)).into_response() } Ok(None) => { (StatusCode::NOT_FOUND, Json(serde_json::json!({ "error": "share not found" }))).into_response() } Err(e) => { tracing::error!("Failed to get share: {}", e); (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": "failed to get share" }))).into_response() } } } #[derive(Debug, Deserialize, Validate)] pub struct UpdateShareRequest { pub permissions: Option>, #[serde(default)] pub active: Option, #[serde(default)] pub expires_days: Option, } pub async fn update_share( State(state): State, Path(id): Path, Extension(claims): Extension, Json(req): Json, ) -> impl IntoResponse { // First get the share let mut share = match state.db.get_share(&id).await { Ok(Some(s)) => s, Ok(None) => { return (StatusCode::NOT_FOUND, Json(serde_json::json!({ "error": "share not found" }))).into_response(); } Err(e) => { tracing::error!("Failed to get share: {}", e); return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": "failed to get share" }))).into_response() } }; // Verify ownership let owner_id = match ObjectId::parse_str(&claims.sub) { Ok(id) => id, Err(_) => { return (StatusCode::BAD_REQUEST, Json(serde_json::json!({ "error": "invalid user ID format" }))).into_response(); } }; if share.owner_id != owner_id { return (StatusCode::FORBIDDEN, Json(serde_json::json!({ "error": "not authorized to modify this share" }))).into_response(); } // Update fields if let Some(permissions) = req.permissions { share.permissions = permissions .into_iter() .filter_map(|p| match p.to_lowercase().as_str() { "read" => Some(Permission::Read), "write" => Some(Permission::Write), "delete" => Some(Permission::Delete), "share" => Some(Permission::Share), "admin" => Some(Permission::Admin), _ => None, }) .collect(); } if let Some(active) = req.active { share.active = active; } if let Some(days) = req.expires_days { share.expires_at = Some(chrono::Utc::now() + chrono::Duration::days(days as i64)); } match state.db.update_share(&share).await { Ok(_) => { let response: ShareResponse = match share.try_into() { Ok(r) => r, Err(_) => { return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": "failed to create share response" }))).into_response(); } }; (StatusCode::OK, Json(response)).into_response() } Err(e) => { tracing::error!("Failed to update share: {}", e); (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": "failed to update share" }))).into_response() } } } pub async fn delete_share( State(state): State, Path(id): Path, Extension(claims): Extension, ) -> impl IntoResponse { // First get the share to verify ownership let share = match state.db.get_share(&id).await { Ok(Some(s)) => s, Ok(None) => { return (StatusCode::NOT_FOUND, Json(serde_json::json!({ "error": "share not found" }))).into_response() } Err(e) => { tracing::error!("Failed to get share: {}", e); return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": "failed to get share" }))).into_response() } }; // Verify ownership let owner_id = match ObjectId::parse_str(&claims.sub) { Ok(id) => id, Err(_) => { return (StatusCode::BAD_REQUEST, Json(serde_json::json!({ "error": "invalid user ID format" }))).into_response(); } }; if share.owner_id != owner_id { return (StatusCode::FORBIDDEN, Json(serde_json::json!({ "error": "not authorized to delete this share" }))).into_response() } match state.db.delete_share(&id).await { Ok(_) => (StatusCode::NO_CONTENT, ()).into_response(), Err(e) => { tracing::error!("Failed to delete share: {}", e); (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": "failed to delete share" }))).into_response() } } }