diff --git a/backend/src/main.rs b/backend/src/main.rs index 11155fb..ff60909 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -2,7 +2,7 @@ use axum::{ extract::{DefaultBodyLimit, Multipart, Path, State}, http::{HeaderMap, StatusCode}, response::{IntoResponse, Json}, - routing::{get, post}, + routing::{get, post, delete}, Router, }; use serde::{Deserialize, Serialize}; @@ -22,16 +22,22 @@ struct AppState { #[derive(Serialize, Deserialize, Clone)] struct SiteConfig { title: String, + subtitle: String, + footer: String, favicon: String, theme: String, + custom_css: String, } impl Default for SiteConfig { fn default() -> Self { Self { title: "Narlblog".to_string(), + subtitle: "A clean, modern blog".to_string(), + footer: "Built with Rust & Astro".to_string(), favicon: "/favicon.svg".to_string(), - theme: "catppuccin-mocha".to_string(), + theme: "mocha".to_string(), + custom_css: "".to_string(), } } } @@ -63,6 +69,12 @@ struct UploadResponse { url: String, } +#[derive(Serialize)] +struct FileInfo { + name: String, + url: String, +} + #[tokio::main] async fn main() { tracing_subscriber::fmt::init(); @@ -92,10 +104,11 @@ async fn main() { let app = Router::new() .route("/api/config", get(get_config).post(update_config)) .route("/api/posts", get(list_posts).post(create_post)) - .route("/api/posts/{slug}", get(get_post)) + .route("/api/posts/{slug}", get(get_post).delete(delete_post)) + .route("/api/uploads", get(list_uploads)) .route("/api/upload", post(upload_file)) .nest_service("/uploads", ServeDir::new(uploads_dir)) - .layer(DefaultBodyLimit::max(10 * 1024 * 1024)) // 10MB limit + .layer(DefaultBodyLimit::max(50 * 1024 * 1024)) // 50MB limit .layer(cors) .with_state(state); @@ -172,6 +185,62 @@ async fn create_post( })) } +async fn delete_post( + State(state): State>, + headers: HeaderMap, + Path(slug): Path, +) -> Result)> { + check_auth(&headers, &state.admin_token)?; + + let file_path = state.data_dir.join("posts").join(format!("{}.md", slug)); + + // Security check to prevent directory traversal + if file_path.parent() != Some(&state.data_dir.join("posts")) { + return Err(( + StatusCode::BAD_REQUEST, + Json(ErrorResponse { error: "Invalid slug".to_string() }), + )); + } + + if file_path.exists() { + fs::remove_file(file_path).map_err(|_| { + (StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse { error: "Delete error".to_string() })) + })?; + Ok(StatusCode::NO_CONTENT) + } else { + Err(( + StatusCode::NOT_FOUND, + Json(ErrorResponse { error: "Post not found".to_string() }), + )) + } +} + +async fn list_uploads( + State(state): State>, + headers: HeaderMap, +) -> Result>, (StatusCode, Json)> { + check_auth(&headers, &state.admin_token)?; + + let uploads_dir = state.data_dir.join("uploads"); + let mut files = Vec::new(); + + if let Ok(entries) = fs::read_dir(uploads_dir) { + for entry in entries.flatten() { + let path = entry.path(); + if path.is_file() { + if let Some(name) = path.file_name().and_then(|n| n.to_str()) { + files.push(FileInfo { + name: name.to_string(), + url: format!("/uploads/{}", name), + }); + } + } + } + } + + Ok(Json(files)) +} + async fn list_posts(State(state): State>) -> impl IntoResponse { let posts_dir = state.data_dir.join("posts"); let mut posts = Vec::new(); @@ -222,9 +291,7 @@ async fn upload_file( ) -> Result, (StatusCode, Json)> { check_auth(&headers, &state.admin_token)?; - while let Some(field) = multipart.next_field().await.map_err(|_| { - (StatusCode::BAD_REQUEST, Json(ErrorResponse { error: "Multipart error".to_string() })) - })? { + while let Ok(Some(field)) = multipart.next_field().await { if let Some(file_name) = field.file_name() { let file_name = slug::slugify(file_name); let uploads_dir = state.data_dir.join("uploads"); @@ -258,4 +325,4 @@ async fn upload_file( StatusCode::BAD_REQUEST, Json(ErrorResponse { error: "No file found".to_string() }), )) -} \ No newline at end of file +} diff --git a/frontend/src/layouts/Layout.astro b/frontend/src/layouts/Layout.astro index 2556eca..2eacd4a 100644 --- a/frontend/src/layouts/Layout.astro +++ b/frontend/src/layouts/Layout.astro @@ -7,12 +7,15 @@ interface Props { const { title } = Astro.props; -const API_URL = (typeof process !== 'undefined' ? process.env.PUBLIC_API_URL : import.meta.env.PUBLIC_API_URL) || 'http://localhost:3000'; +const API_URL = process.env.PUBLIC_API_URL || 'http://backend:3000'; let siteConfig = { title: "Narlblog", + subtitle: "A clean, modern blog", + footer: "Built with Rust & Astro", favicon: "/favicon.svg", - theme: "catppuccin-mocha" + theme: "mocha", + custom_css: "" }; try { @@ -34,18 +37,21 @@ try { {title} | {siteConfig.title} + {siteConfig.custom_css &&