diff --git a/backend/src/auth.rs b/backend/src/auth.rs index 861bf38..5a89fbd 100644 --- a/backend/src/auth.rs +++ b/backend/src/auth.rs @@ -1,6 +1,6 @@ +use crate::error::AppError; use axum::http::HeaderMap; use tracing::warn; -use crate::error::AppError; pub fn check_auth(headers: &HeaderMap, admin_token: &str) -> Result<(), AppError> { let auth_header = headers.get("Authorization").and_then(|h| h.to_str().ok()); diff --git a/backend/src/error.rs b/backend/src/error.rs index 363c8c4..97a7d28 100644 --- a/backend/src/error.rs +++ b/backend/src/error.rs @@ -1,7 +1,7 @@ use axum::{ + Json, http::StatusCode, response::{IntoResponse, Response}, - Json, }; use crate::models::ErrorResponse; @@ -38,4 +38,4 @@ where fn from(err: E) -> Self { AppError::Internal("Internal Server Error".to_string(), Some(err.to_string())) } -} \ No newline at end of file +} diff --git a/backend/src/handlers/config.rs b/backend/src/handlers/config.rs index 0f949e9..be6c1ea 100644 --- a/backend/src/handlers/config.rs +++ b/backend/src/handlers/config.rs @@ -1,18 +1,8 @@ -use axum::{ - extract::State, - http::HeaderMap, - response::IntoResponse, - Json, -}; +use axum::{Json, extract::State, http::HeaderMap, response::IntoResponse}; use std::{fs, sync::Arc}; use tracing::error; -use crate::{ - auth::check_auth, - error::AppError, - models::SiteConfig, - AppState, -}; +use crate::{AppState, auth::check_auth, error::AppError, models::SiteConfig}; pub async fn get_config(State(state): State>) -> impl IntoResponse { let config_path = state.data_dir.join("config.json"); @@ -20,7 +10,7 @@ pub async fn get_config(State(state): State>) -> impl IntoResponse .ok() .and_then(|c| serde_json::from_str::(&c).ok()) .unwrap_or_default(); - + Json(config) } @@ -43,4 +33,4 @@ pub async fn update_config( })?; Ok(Json(payload)) -} \ No newline at end of file +} diff --git a/backend/src/handlers/posts.rs b/backend/src/handlers/posts.rs index fd7870f..b9755b1 100644 --- a/backend/src/handlers/posts.rs +++ b/backend/src/handlers/posts.rs @@ -1,17 +1,17 @@ use axum::{ + Json, extract::{Path, State}, http::{HeaderMap, StatusCode}, response::IntoResponse, - Json, }; use std::{fs, sync::Arc}; use tracing::{error, info, warn}; use crate::{ + AppState, auth::check_auth, error::AppError, models::{CreatePostRequest, PostDetail, PostInfo}, - AppState, }; pub async fn create_post( @@ -25,8 +25,11 @@ pub async fn create_post( return Err(AppError::BadRequest("Invalid slug".to_string())); } - let file_path = state.data_dir.join("posts").join(format!("{}.md", payload.slug)); - + let file_path = state + .data_dir + .join("posts") + .join(format!("{}.md", payload.slug)); + let mut file_content = String::new(); if let Some(ref summary) = payload.summary { if !summary.trim().is_empty() { @@ -59,7 +62,7 @@ pub async fn delete_post( 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())); @@ -99,13 +102,17 @@ pub async fn list_posts(State(state): State>) -> impl IntoResponse let body = parts[2]; let clean_content = body.replace("#", "").replace("\n", " "); excerpt = clean_content.chars().take(200).collect::(); - if clean_content.len() > 200 { excerpt.push_str("..."); } + if clean_content.len() > 200 { + excerpt.push_str("..."); + } } } } else { let clean_content = content.replace("#", "").replace("\n", " "); excerpt = clean_content.chars().take(200).collect::(); - if clean_content.len() > 200 { excerpt.push_str("..."); } + if clean_content.len() > 200 { + excerpt.push_str("..."); + } } } posts.push(PostInfo { @@ -125,7 +132,7 @@ pub async fn get_post( Path(slug): Path, ) -> Result, AppError> { let file_path = state.data_dir.join("posts").join(format!("{}.md", slug)); - + match fs::read_to_string(&file_path) { Ok(raw_content) => { let mut summary = None; @@ -145,8 +152,12 @@ pub async fn get_post( } } - Ok(Json(PostDetail { slug, summary, content })) - }, + Ok(Json(PostDetail { + slug, + summary, + content, + })) + } Err(_) => Err(AppError::NotFound("Post not found".to_string())), } -} \ No newline at end of file +} diff --git a/backend/src/handlers/upload.rs b/backend/src/handlers/upload.rs index b1e4c54..49ebf12 100644 --- a/backend/src/handlers/upload.rs +++ b/backend/src/handlers/upload.rs @@ -1,17 +1,16 @@ use axum::{ + Json, extract::{Multipart, State}, http::HeaderMap, - response::IntoResponse, - Json, }; use std::{fs, sync::Arc}; use tracing::{error, info, warn}; use crate::{ + AppState, auth::check_auth, error::AppError, models::{FileInfo, UploadResponse}, - AppState, }; pub async fn list_uploads( @@ -57,12 +56,12 @@ pub async fn upload_file( info!("Processing upload for: {}", file_name); let slugified_name = slug::slugify(&file_name); - + let extension = std::path::Path::new(&file_name) .extension() .and_then(|e| e.to_str()) .unwrap_or(""); - + let final_name = if !extension.is_empty() { format!("{}.{}", slugified_name, extension) } else { @@ -79,7 +78,12 @@ pub async fn upload_file( file_path }; - let final_name_str = final_path.file_name().unwrap().to_str().unwrap().to_string(); + let final_name_str = final_path + .file_name() + .unwrap() + .to_str() + .unwrap() + .to_string(); let data = match field.bytes().await { Ok(bytes) => bytes, @@ -91,7 +95,10 @@ pub async fn upload_file( if let Err(e) = fs::write(&final_path, &data) { error!("Failed to write file to {:?}: {}", final_path, e); - return Err(AppError::Internal("Write error".to_string(), Some(e.to_string()))); + return Err(AppError::Internal( + "Write error".to_string(), + Some(e.to_string()), + )); } info!("File uploaded successfully to {:?}", final_path); @@ -102,4 +109,4 @@ pub async fn upload_file( warn!("Upload failed: no file found in multipart stream"); Err(AppError::BadRequest("No file found".to_string())) -} \ No newline at end of file +} diff --git a/backend/src/main.rs b/backend/src/main.rs index edb3635..4f18c29 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -4,9 +4,9 @@ pub mod handlers; pub mod models; use axum::{ - extract::DefaultBodyLimit, - routing::{delete, get, post}, Router, + extract::DefaultBodyLimit, + routing::{get, post}, }; use std::{env, fs, path::PathBuf, sync::Arc}; use tower_http::{ @@ -54,9 +54,18 @@ async fn main() { .allow_headers(Any); let app = Router::new() - .route("/api/config", get(handlers::config::get_config).post(handlers::config::update_config)) - .route("/api/posts", get(handlers::posts::list_posts).post(handlers::posts::create_post)) - .route("/api/posts/{slug}", get(handlers::posts::get_post).delete(handlers::posts::delete_post)) + .route( + "/api/config", + get(handlers::config::get_config).post(handlers::config::update_config), + ) + .route( + "/api/posts", + get(handlers::posts::list_posts).post(handlers::posts::create_post), + ) + .route( + "/api/posts/{slug}", + get(handlers::posts::get_post).delete(handlers::posts::delete_post), + ) .route("/api/uploads", get(handlers::upload::list_uploads)) .route("/api/upload", post(handlers::upload::upload_file)) .nest_service("/uploads", ServeDir::new(uploads_dir)) diff --git a/backend/src/models.rs b/backend/src/models.rs index 3730edf..be67440 100644 --- a/backend/src/models.rs +++ b/backend/src/models.rs @@ -18,7 +18,8 @@ impl Default for SiteConfig { title: "Narlblog".to_string(), subtitle: "A clean, modern blog".to_string(), welcome_title: "Welcome to my blog".to_string(), - welcome_subtitle: "Thoughts on software, design, and building things with Rust and Astro.".to_string(), + welcome_subtitle: + "Thoughts on software, design, and building things with Rust and Astro.".to_string(), footer: "Built with Rust & Astro".to_string(), favicon: "/favicon.svg".to_string(), theme: "mocha".to_string(), diff --git a/frontend/src/components/ThemeSwitcher.astro b/frontend/src/components/ThemeSwitcher.astro new file mode 100644 index 0000000..c27954a --- /dev/null +++ b/frontend/src/components/ThemeSwitcher.astro @@ -0,0 +1,25 @@ +--- +const { defaultTheme } = Astro.props; +--- + + + + diff --git a/frontend/src/layouts/Layout.astro b/frontend/src/layouts/Layout.astro index 7c418c1..458db84 100644 --- a/frontend/src/layouts/Layout.astro +++ b/frontend/src/layouts/Layout.astro @@ -1,5 +1,6 @@ --- import '../styles/global.css'; +import ThemeSwitcher from '../components/ThemeSwitcher.astro'; interface Props { title: string; @@ -45,7 +46,7 @@ try { -
+
@@ -62,13 +63,7 @@ try {
Home Admin - +
@@ -83,21 +78,5 @@ try { © {new Date().getFullYear()} {siteConfig.title}
- - diff --git a/frontend/src/pages/admin/editor.astro b/frontend/src/pages/admin/editor.astro index e38254d..5c78e8d 100644 --- a/frontend/src/pages/admin/editor.astro +++ b/frontend/src/pages/admin/editor.astro @@ -3,7 +3,7 @@ import AdminLayout from '../../layouts/AdminLayout.astro'; --- -

Create/Edit post.

+

Create/Edit post.