Enhance RustyCMS: Update .gitignore to include demo assets, improve admin UI dependency management, and add new translations for asset management. Implement asset date filtering and enhance content forms with asset previews. Introduce caching mechanisms for improved performance and add support for draft status in content entries.
This commit is contained in:
@@ -14,7 +14,7 @@ use tokio::sync::RwLock;
|
||||
use crate::schema::types::{SchemaDefinition, VALID_FIELD_TYPES};
|
||||
use crate::schema::validator;
|
||||
use crate::schema::SchemaRegistry;
|
||||
use crate::store::query::QueryParams;
|
||||
use crate::store::query::{QueryParams, StatusFilter, entry_is_draft};
|
||||
use crate::store::ContentStore;
|
||||
use crate::store::slug;
|
||||
|
||||
@@ -368,6 +368,7 @@ pub async fn list_entries(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(collection): Path<String>,
|
||||
Query(params): Query<HashMap<String, String>>,
|
||||
headers: HeaderMap,
|
||||
) -> Result<Json<Value>, ApiError> {
|
||||
let registry = state.registry.read().await;
|
||||
let schema = registry
|
||||
@@ -411,7 +412,15 @@ pub async fn list_entries(
|
||||
let entries = state.store.list(&collection, locale_ref).await.map_err(ApiError::from)?;
|
||||
|
||||
let resolve = parse_resolve(list_params.get("_resolve").map(|s| s.as_str()));
|
||||
let query = QueryParams::from_map(list_params);
|
||||
// When API key is required but not sent, show only published entries. Otherwise use _status param.
|
||||
let status_override = if state.api_key.as_ref().is_some()
|
||||
&& auth::token_from_headers(&headers).as_deref() != state.api_key.as_ref().map(String::as_str)
|
||||
{
|
||||
Some(StatusFilter::Published)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let query = QueryParams::from_map_with_status(list_params, status_override);
|
||||
let mut result = query.apply(entries);
|
||||
|
||||
for item in result.items.iter_mut() {
|
||||
@@ -461,29 +470,36 @@ pub async fn get_entry(
|
||||
let resolve_key = params.get("_resolve").map(|s| s.as_str()).unwrap_or("");
|
||||
let cache_key = cache::entry_cache_key(&collection, &slug, resolve_key, locale_ref);
|
||||
if let Some(ref cached) = state.cache.get(&cache_key).await {
|
||||
let json_str = serde_json::to_string(cached).unwrap_or_default();
|
||||
let mut hasher = DefaultHasher::new();
|
||||
json_str.hash(&mut hasher);
|
||||
let etag_plain = format!("\"{:016x}\"", hasher.finish());
|
||||
let etag_header =
|
||||
HeaderValue::from_str(&etag_plain).unwrap_or_else(|_| HeaderValue::from_static("\"0\""));
|
||||
let if_none_match = headers
|
||||
.get("if-none-match")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.map(|s| s.trim().trim_matches('"').to_string());
|
||||
if if_none_match.as_deref() == Some(etag_plain.trim_matches('"')) {
|
||||
// Don't serve cached draft to unauthenticated requests.
|
||||
let is_authenticated = state.api_key.as_ref().is_none()
|
||||
|| auth::token_from_headers(&headers).as_deref() == state.api_key.as_ref().map(String::as_str);
|
||||
if !is_authenticated && entry_is_draft(cached) {
|
||||
// Fall through to load from store (will 404 if draft).
|
||||
} else {
|
||||
let json_str = serde_json::to_string(cached).unwrap_or_default();
|
||||
let mut hasher = DefaultHasher::new();
|
||||
json_str.hash(&mut hasher);
|
||||
let etag_plain = format!("\"{:016x}\"", hasher.finish());
|
||||
let etag_header =
|
||||
HeaderValue::from_str(&etag_plain).unwrap_or_else(|_| HeaderValue::from_static("\"0\""));
|
||||
let if_none_match = headers
|
||||
.get("if-none-match")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.map(|s| s.trim().trim_matches('"').to_string());
|
||||
if if_none_match.as_deref() == Some(etag_plain.trim_matches('"')) {
|
||||
return Ok((
|
||||
StatusCode::NOT_MODIFIED,
|
||||
[("ETag", etag_header.clone())],
|
||||
)
|
||||
.into_response());
|
||||
}
|
||||
return Ok((
|
||||
StatusCode::NOT_MODIFIED,
|
||||
[("ETag", etag_header.clone())],
|
||||
StatusCode::OK,
|
||||
[("ETag", etag_header)],
|
||||
Json(cached.clone()),
|
||||
)
|
||||
.into_response());
|
||||
}
|
||||
return Ok((
|
||||
StatusCode::OK,
|
||||
[("ETag", etag_header)],
|
||||
Json(cached.clone()),
|
||||
)
|
||||
.into_response());
|
||||
}
|
||||
|
||||
let entry = state
|
||||
@@ -495,6 +511,17 @@ pub async fn get_entry(
|
||||
ApiError::NotFound(format!("Entry '{}' not found in '{}'", slug, collection))
|
||||
})?;
|
||||
|
||||
// When no API key is sent, hide draft entries (return 404).
|
||||
if state.api_key.as_ref().is_some()
|
||||
&& auth::token_from_headers(&headers).as_deref() != state.api_key.as_ref().map(String::as_str)
|
||||
&& entry_is_draft(&entry)
|
||||
{
|
||||
return Err(ApiError::NotFound(format!(
|
||||
"Entry '{}' not found in '{}'",
|
||||
slug, collection
|
||||
)));
|
||||
}
|
||||
|
||||
let resolve = parse_resolve(params.get("_resolve").map(|s| s.as_str()));
|
||||
let mut formatted = format_references(
|
||||
entry,
|
||||
@@ -509,10 +536,10 @@ pub async fn get_entry(
|
||||
expand_asset_urls(&mut formatted, &state.base_url);
|
||||
}
|
||||
|
||||
state
|
||||
.cache
|
||||
.set(cache_key, formatted.clone())
|
||||
.await;
|
||||
// Only cache published entries so unauthenticated requests never see cached drafts.
|
||||
if !entry_is_draft(&formatted) {
|
||||
state.cache.set(cache_key, formatted.clone()).await;
|
||||
}
|
||||
|
||||
let json_str = serde_json::to_string(&formatted).unwrap_or_default();
|
||||
let mut hasher = DefaultHasher::new();
|
||||
@@ -581,9 +608,19 @@ pub async fn create_entry(
|
||||
slug::validate_slug(&slug_raw).map_err(ApiError::BadRequest)?;
|
||||
let slug = slug::normalize_slug(&slug_raw);
|
||||
|
||||
// Remove _slug from content data
|
||||
// Remove _slug from content data; validate and default _status (publish/draft)
|
||||
if let Some(obj) = body.as_object_mut() {
|
||||
obj.remove("_slug");
|
||||
if let Some(v) = obj.get("_status") {
|
||||
let s = v.as_str().unwrap_or("");
|
||||
if s != "draft" && s != "published" {
|
||||
return Err(ApiError::BadRequest(
|
||||
"Field '_status' must be \"draft\" or \"published\"".to_string(),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
obj.insert("_status".to_string(), Value::String("published".to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
// Apply defaults and auto-generated values
|
||||
@@ -639,7 +676,7 @@ pub async fn create_entry(
|
||||
.await
|
||||
.map_err(ApiError::from)?
|
||||
.unwrap();
|
||||
let mut formatted = format_references(
|
||||
let formatted = format_references(
|
||||
entry,
|
||||
&schema,
|
||||
state.store.as_ref(),
|
||||
@@ -682,9 +719,17 @@ pub async fn update_entry(
|
||||
)));
|
||||
}
|
||||
|
||||
// Remove _slug if present in body
|
||||
// Remove _slug if present; validate _status when present
|
||||
if let Some(obj) = body.as_object_mut() {
|
||||
obj.remove("_slug");
|
||||
if let Some(v) = obj.get("_status") {
|
||||
let s = v.as_str().unwrap_or("");
|
||||
if s != "draft" && s != "published" {
|
||||
return Err(ApiError::BadRequest(
|
||||
"Field '_status' must be \"draft\" or \"published\"".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize reference arrays: objects with _slug → slug strings (so clients can send resolved refs)
|
||||
@@ -754,7 +799,7 @@ pub async fn update_entry(
|
||||
.await
|
||||
.map_err(ApiError::from)?
|
||||
.unwrap();
|
||||
let mut formatted = format_references(
|
||||
let formatted = format_references(
|
||||
entry,
|
||||
&schema,
|
||||
state.store.as_ref(),
|
||||
|
||||
Reference in New Issue
Block a user