use axum::http::StatusCode;
use chrono::Utc;
use tracing::info;
use vti_common::audit::{AuditEvent, CommunityThemeUpdatedData};
use vti_common::error::AppError;
use crate::community::theme::{
MAX_STYLESHEET_BYTES, ProfileViolation, StoredTheme, check_stylesheet, load_theme, store_theme,
stylesheet_digest,
};
use crate::server::AppState;
pub(crate) struct ThemeUpdate {
pub stylesheet: String,
pub stored: bool,
pub updated_at: Option<chrono::DateTime<Utc>>,
}
pub(crate) enum ThemeUpdateError {
OutsideProfile(ProfileViolation),
App(AppError),
}
impl From<AppError> for ThemeUpdateError {
fn from(e: AppError) -> Self {
Self::App(e)
}
}
pub(crate) async fn update_theme(
state: &AppState,
actor: &str,
stylesheet: &str,
dry_run: bool,
) -> Result<ThemeUpdate, ThemeUpdateError> {
if stylesheet.len() > MAX_STYLESHEET_BYTES {
return Err(AppError::Validation(format!(
"stylesheet is {} bytes; the bound is {MAX_STYLESHEET_BYTES}",
stylesheet.len()
))
.into());
}
let normalised = check_stylesheet(stylesheet).map_err(ThemeUpdateError::OutsideProfile)?;
let current = load_theme(&state.community_ks).await?;
if dry_run {
return Ok(ThemeUpdate {
stylesheet: normalised,
stored: false,
updated_at: None,
});
}
let unchanged = match ¤t {
Some(StoredTheme { stylesheet, .. }) => *stylesheet == normalised,
None => normalised.is_empty(),
};
if unchanged {
return Ok(ThemeUpdate {
stylesheet: normalised,
stored: false,
updated_at: current.map(|t| t.updated_at),
});
}
let writer = state
.audit_writer
.as_ref()
.ok_or_else(|| AppError::ServiceError {
status: StatusCode::SERVICE_UNAVAILABLE,
message: "audit writer not configured".into(),
})?;
let removed = normalised.is_empty();
let digest = (!removed).then(|| stylesheet_digest(&normalised));
writer
.write(
actor,
None,
AuditEvent::CommunityThemeUpdated(CommunityThemeUpdatedData {
digest: digest.clone(),
bytes: u32::try_from(normalised.len()).unwrap_or(u32::MAX),
}),
)
.await?;
let row = store_theme(&state.community_ks, &normalised, Utc::now()).await?;
info!(removed, digest = ?digest, "community theme updated");
Ok(ThemeUpdate {
stylesheet: normalised,
stored: true,
updated_at: row.map(|r| r.updated_at),
})
}