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::auth::{AdminAuth, AuthClaims};
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 {
#[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 mediator_did: Option<String>,
}
#[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());
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,
mediator_did,
}))
}
#[utoipa::path(
get, path = "/community/profile", tag = "community",
security(("bearer_jwt" = [])),
responses(
(status = 200, description = "Community profile + registry status", body = CommunityProfileResponse),
(status = 401, description = "Missing or invalid bearer token"),
(status = 404, description = "Community profile not initialised"),
),
)]
pub async fn get_profile(
_auth: AuthClaims,
State(state): State<AppState>,
) -> Result<Json<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(Json(CommunityProfileResponse {
profile,
registry_status,
}))
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
#[derive(utoipa::ToSchema)]
pub struct UpdateProfileResponse {
pub profile: CommunityProfile,
pub fields_changed: Vec<String>,
}
#[utoipa::path(
put, path = "/community/profile", tag = "community",
security(("bearer_jwt" = [])),
request_body = CommunityProfileUpdate,
responses(
(status = 200, description = "Updated profile + the fields that changed", body = UpdateProfileResponse),
(status = 401, description = "Missing or invalid bearer token"),
(status = 403, description = "Caller is not an admin"),
(status = 404, description = "Community profile not initialised"),
(status = 503, description = "Audit writer not configured — change refused"),
),
)]
pub async fn put_profile(
admin: AdminAuth,
State(state): State<AppState>,
Json(update): Json<CommunityProfileUpdate>,
) -> Result<(StatusCode, Json<UpdateProfileResponse>), AppError> {
let mut profile = load_profile(&state.community_ks).await?.ok_or_else(|| {
AppError::NotFound("community profile not initialised — cannot PUT before bootstrap".into())
})?;
let fields_changed = update.apply(&mut profile)?;
if fields_changed.is_empty() {
return Ok((
StatusCode::OK,
Json(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(
&admin.0.did,
None,
AuditEvent::CommunityProfileUpdated(CommunityProfileUpdatedData {
fields_changed: fields_changed.clone(),
}),
)
.await?;
Ok((
StatusCode::OK,
Json(UpdateProfileResponse {
profile,
fields_changed,
}),
))
}
#[cfg(test)]
mod tests {
}