Files
narlblog/backend/src/handlers/posts.rs
2026-03-26 00:50:11 +01:00

105 lines
3.4 KiB
Rust

use axum::{
extract::{Path, State},
http::{HeaderMap, StatusCode},
response::IntoResponse,
Json,
};
use std::{fs, sync::Arc};
use tracing::{error, info, warn};
use crate::{
auth::check_auth,
error::AppError,
models::{CreatePostRequest, PostDetail, PostInfo},
AppState,
};
pub async fn create_post(
State(state): State<Arc<AppState>>,
headers: HeaderMap,
Json(payload): Json<CreatePostRequest>,
) -> Result<Json<PostDetail>, AppError> {
check_auth(&headers, &state.admin_token)?;
if payload.slug.contains('/') || payload.slug.contains('\\') || payload.slug.contains("..") {
return Err(AppError::BadRequest("Invalid slug".to_string()));
}
let file_path = state.data_dir.join("posts").join(format!("{}.md", payload.slug));
fs::write(&file_path, &payload.content).map_err(|e| {
error!("Write error for post {}: {}", payload.slug, e);
AppError::Internal("Write error".to_string(), Some(e.to_string()))
})?;
info!("Post created/updated: {}", payload.slug);
Ok(Json(PostDetail {
slug: payload.slug,
content: payload.content,
}))
}
pub async fn delete_post(
State(state): State<Arc<AppState>>,
headers: HeaderMap,
Path(slug): Path<String>,
) -> Result<StatusCode, AppError> {
check_auth(&headers, &state.admin_token)?;
let file_path = state.data_dir.join("posts").join(format!("{}.md", slug));
info!("Attempting to delete post at: {:?}", file_path);
if !file_path.exists() {
warn!("Post not found for deletion: {}", slug);
return Err(AppError::NotFound("Post not found".to_string()));
}
fs::remove_file(file_path).map_err(|e| {
error!("Delete error for post {}: {}", slug, e);
AppError::Internal("Delete error".to_string(), Some(e.to_string()))
})?;
info!("Post deleted: {}", slug);
Ok(StatusCode::NO_CONTENT)
}
pub async fn list_posts(State(state): State<Arc<AppState>>) -> impl IntoResponse {
let posts_dir = state.data_dir.join("posts");
let mut posts = Vec::new();
if let Ok(entries) = fs::read_dir(posts_dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) == Some("md") {
if let Some(slug) = path.file_stem().and_then(|s| s.to_str()) {
let mut excerpt = String::new();
if let Ok(content) = fs::read_to_string(&path) {
let clean_content = content.replace("#", "").replace("\n", " ");
excerpt = clean_content.chars().take(200).collect::<String>();
if clean_content.len() > 200 {
excerpt.push_str("...");
}
}
posts.push(PostInfo {
slug: slug.to_string(),
excerpt: excerpt.trim().to_string(),
});
}
}
}
}
Json(posts)
}
pub async fn get_post(
State(state): State<Arc<AppState>>,
Path(slug): Path<String>,
) -> Result<Json<PostDetail>, AppError> {
let file_path = state.data_dir.join("posts").join(format!("{}.md", slug));
match fs::read_to_string(&file_path) {
Ok(content) => Ok(Json(PostDetail { slug, content })),
Err(_) => Err(AppError::NotFound("Post not found".to_string())),
}
}