use axum::Json;
use axum::extract::State;
use axum::http::StatusCode;
use chrono::{DateTime, Utc};
use serde::Serialize;
use tracing::info;
use vti_common::audit::{AuditEvent, CommunityProfileUpdatedData};
use vti_common::error::AppError;
use crate::community::{CommunityProfile, CommunityProfileUpdate, load_profile, store_profile};
use crate::registry::HealthStatus;
use crate::server::AppState;
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
#[derive(utoipa::ToSchema)]
pub struct CommunityProfileResponse {
pub profile: ProfileWithStatus,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
#[derive(utoipa::ToSchema)]
pub struct ProfileWithStatus {
#[serde(flatten)]
pub profile: CommunityProfile,
pub registry_status: HealthStatus,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
#[derive(utoipa::ToSchema)]
pub struct PublicCommunityProfile {
pub community_did: String,
pub name: String,
pub description: String,
pub logo_url: Option<String>,
pub public_url: Option<String>,
pub contact_email: Option<String>,
pub language: String,
pub created_at: DateTime<Utc>,
pub personhood: crate::community::PersonhoodGovernance,
pub mediator_did: Option<String>,
pub transports: Vec<crate::transport_capability::TransportStatus>,
}
#[utoipa::path(
get, path = "/community/public-profile", tag = "community",
responses(
(status = 200, description = "Public community profile", body = PublicCommunityProfile),
(status = 404, description = "Community profile not initialised"),
),
)]
pub async fn get_public_profile(
State(state): State<AppState>,
) -> Result<Json<PublicCommunityProfile>, AppError> {
let profile = load_profile(&state.community_ks)
.await?
.ok_or_else(|| AppError::NotFound("community profile not initialised".into()))?;
let mediator_did = state
.config
.read()
.await
.messaging
.as_ref()
.map(|m| m.mediator_did.clone());
let messaging_connected = matches!(
state.didcomm.get().map(|m| m.service.status()),
Some(affinidi_messaging_delivery::MessagingStatus::Connected)
);
let transports = match crate::transport_capability::resolved_capabilities(
state.did_resolver.as_ref(),
&profile.community_did,
)
.await
{
Some(caps) => {
crate::transport_capability::public_transport_view_for_build(&caps, messaging_connected)
}
None => Vec::new(),
};
Ok(Json(PublicCommunityProfile {
community_did: profile.community_did,
name: profile.name,
description: profile.description,
logo_url: profile.logo_url,
public_url: profile.public_url,
contact_email: profile.contact_email,
language: profile.language,
created_at: profile.created_at,
personhood: profile.personhood,
mediator_did,
transports,
}))
}
pub(crate) async fn show_profile(state: &AppState) -> Result<CommunityProfileResponse, AppError> {
let profile = load_profile(&state.community_ks)
.await?
.ok_or_else(|| AppError::NotFound("community profile not initialised".into()))?;
let registry_status = state.registry_health.status().await;
Ok(CommunityProfileResponse {
profile: ProfileWithStatus {
profile,
registry_status,
},
})
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
#[derive(utoipa::ToSchema)]
pub struct UpdateProfileResponse {
pub profile: CommunityProfile,
pub fields_changed: Vec<String>,
}
pub const PROFILE_UPDATE_ERR_VALIDATION_FAILED: &str =
trust_tasks_rs::specs::vtc::community::profile::update::v0_1::error_codes::VALIDATION_FAILED
.code;
pub(crate) async fn update_profile_inner(
state: &AppState,
actor_did: &str,
update: CommunityProfileUpdate,
) -> Result<UpdateProfileResponse, crate::error::TaskError> {
use crate::error::TaskError;
let mut profile = load_profile(&state.community_ks).await?.ok_or_else(|| {
AppError::NotFound("community profile not initialised — cannot PUT before bootstrap".into())
})?;
let prior = profile.clone();
let fields_changed = update.apply(&mut profile).map_err(|e| match e {
e @ AppError::Validation(_) => TaskError::declared(PROFILE_UPDATE_ERR_VALIDATION_FAILED, e),
e => TaskError::App(e),
})?;
if fields_changed.is_empty() {
return Ok(UpdateProfileResponse {
profile,
fields_changed,
});
}
let audit_writer = state
.audit_writer
.as_ref()
.ok_or_else(|| AppError::ServiceError {
status: StatusCode::SERVICE_UNAVAILABLE,
message: "audit writer not configured".into(),
})?;
store_profile(&state.community_ks, &profile).await?;
info!(
community_did = %profile.community_did,
fields_changed = ?fields_changed,
"community profile updated"
);
audit_writer
.write(
actor_did,
None,
AuditEvent::CommunityProfileUpdated(CommunityProfileUpdatedData {
fields_changed: fields_changed.clone(),
changes: crate::community::profile::profile_changes(Some(&prior), &profile),
}),
)
.await?;
Ok(UpdateProfileResponse {
profile,
fields_changed,
})
}
#[cfg(test)]
mod tests {
}