use crate::auth::jwt::Claims; use crate::config::AppState; use crate::models::health_stats::{HealthStatResponse, HealthStatistic}; use crate::models::medication::EncryptedFieldWire; use axum::{ extract::{Path, Query, State}, http::StatusCode, response::IntoResponse, Extension, Json, }; use mongodb::bson::oid::ObjectId; use serde::Deserialize; #[derive(Debug, Deserialize)] pub struct CreateHealthStatRequest { pub encrypted_data: EncryptedFieldWire, /// Which profile this stat belongs to. pub profile_id: String, pub recorded_at: Option, } #[derive(Debug, Deserialize, Default)] pub struct ListHealthStatsQuery { pub profile_id: Option, } #[derive(Debug, Deserialize)] pub struct UpdateHealthStatRequest { pub encrypted_data: EncryptedFieldWire, } pub async fn create_health_stat( State(state): State, Extension(claims): Extension, Json(req): Json, ) -> impl IntoResponse { let repo = match state.health_stats_repo.as_ref() { Some(r) => r, None => { return ( StatusCode::SERVICE_UNAVAILABLE, Json(serde_json::json!({ "error": "health stats unavailable" })), ) .into_response() } }; let stat = HealthStatistic { id: None, user_id: claims.sub.clone(), profile_id: req.profile_id, encrypted_data: req.encrypted_data, recorded_at: req .recorded_at .unwrap_or_else(|| chrono::Utc::now().to_rfc3339()), }; match repo.create(&stat).await { Ok(Some(created)) => { (StatusCode::CREATED, Json(HealthStatResponse::from(created))).into_response() } Ok(None) => ( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": "failed to create" })), ) .into_response(), Err(e) => { eprintln!("Error creating health stat: {:?}", e); ( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": "database error" })), ) .into_response() } } } pub async fn list_health_stats( State(state): State, Extension(claims): Extension, Query(query): Query, ) -> impl IntoResponse { let repo = match state.health_stats_repo.as_ref() { Some(r) => r, None => { return ( StatusCode::SERVICE_UNAVAILABLE, Json(serde_json::json!({ "error": "health stats unavailable" })), ) .into_response() } }; // Resolve the effective owner. If a profile_id is specified, prefer direct // ownership (the caller's own data for that profile); otherwise fall through // to the share-gate (admits an active share recipient). profile_id is a // free-form string at create time and may not map to a Profile document. let owner = match &query.profile_id { Some(pid) => { let has_own = repo .find_by_user_filtered(&claims.sub, Some(pid)) .await .map(|v| !v.is_empty()) .unwrap_or(false); if has_own { claims.sub.clone() } else { match crate::handlers::profile_share::authorize_profile_read(&state, &claims, pid) .await { Ok(o) => o, Err(resp) => return resp.into_response(), } } } None => claims.sub.clone(), }; match repo .find_by_user_filtered(&owner, query.profile_id.as_deref()) .await { Ok(stats) => { let resp: Vec = stats.into_iter().map(Into::into).collect(); (StatusCode::OK, Json(resp)).into_response() } Err(_) => ( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": "database error" })), ) .into_response(), } } pub async fn get_health_stat( State(state): State, Extension(claims): Extension, Path(id): Path, ) -> impl IntoResponse { let repo = match state.health_stats_repo.as_ref() { Some(r) => r, None => { return ( StatusCode::SERVICE_UNAVAILABLE, Json(serde_json::json!({ "error": "health stats unavailable" })), ) .into_response() } }; let object_id = match ObjectId::parse_str(&id) { Ok(oid) => oid, Err(_) => { return ( StatusCode::BAD_REQUEST, Json(serde_json::json!({ "error": "invalid id" })), ) .into_response() } }; match repo.find_by_id(&object_id).await { Ok(Some(stat)) => { // Authorization: direct owner (user_id == caller) OR share-gate on // the stat's profile (admits an active share recipient). if stat.user_id == claims.sub { return (StatusCode::OK, Json(HealthStatResponse::from(stat))).into_response(); } match crate::handlers::profile_share::authorize_profile_read( &state, &claims, &stat.profile_id, ) .await { Ok(_) => (StatusCode::OK, Json(HealthStatResponse::from(stat))).into_response(), Err(resp) => resp.into_response(), } } Ok(None) => ( StatusCode::NOT_FOUND, Json(serde_json::json!({ "error": "not found" })), ) .into_response(), Err(_) => ( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": "database error" })), ) .into_response(), } } pub async fn update_health_stat( State(state): State, Extension(claims): Extension, Path(id): Path, Json(req): Json, ) -> impl IntoResponse { let repo = match state.health_stats_repo.as_ref() { Some(r) => r, None => { return ( StatusCode::SERVICE_UNAVAILABLE, Json(serde_json::json!({ "error": "health stats unavailable" })), ) .into_response() } }; let object_id = match ObjectId::parse_str(&id) { Ok(oid) => oid, Err(_) => { return ( StatusCode::BAD_REQUEST, Json(serde_json::json!({ "error": "invalid id" })), ) .into_response() } }; let existing = match repo.find_by_id(&object_id).await { Ok(Some(s)) => s, Ok(None) => { return ( StatusCode::NOT_FOUND, Json(serde_json::json!({ "error": "not found" })), ) .into_response() } Err(_) => { return ( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": "database error" })), ) .into_response() } }; if existing.user_id != claims.sub { return (StatusCode::FORBIDDEN, "Access denied").into_response(); } match repo .update_encrypted_data(&object_id, req.encrypted_data) .await { Ok(Some(updated)) => { (StatusCode::OK, Json(HealthStatResponse::from(updated))).into_response() } Ok(None) => ( StatusCode::NOT_FOUND, Json(serde_json::json!({ "error": "not found" })), ) .into_response(), Err(_) => ( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": "database error" })), ) .into_response(), } } pub async fn delete_health_stat( State(state): State, Extension(claims): Extension, Path(id): Path, ) -> impl IntoResponse { let repo = match state.health_stats_repo.as_ref() { Some(r) => r, None => { return ( StatusCode::SERVICE_UNAVAILABLE, Json(serde_json::json!({ "error": "health stats unavailable" })), ) .into_response() } }; let object_id = match ObjectId::parse_str(&id) { Ok(oid) => oid, Err(_) => { return ( StatusCode::BAD_REQUEST, Json(serde_json::json!({ "error": "invalid id" })), ) .into_response() } }; let stat = match repo.find_by_id(&object_id).await { Ok(Some(s)) => s, Ok(None) => { return ( StatusCode::NOT_FOUND, Json(serde_json::json!({ "error": "not found" })), ) .into_response() } Err(_) => { return ( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": "database error" })), ) .into_response() } }; if stat.user_id != claims.sub { return (StatusCode::FORBIDDEN, "Access denied").into_response(); } match repo.delete(&object_id).await { Ok(true) => StatusCode::NO_CONTENT.into_response(), _ => ( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": "database error" })), ) .into_response(), } }