diff --git a/backend/src/main.rs b/backend/src/main.rs index 40fd0b0..11155fb 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -19,6 +19,23 @@ struct AppState { data_dir: PathBuf, } +#[derive(Serialize, Deserialize, Clone)] +struct SiteConfig { + title: String, + favicon: String, + theme: String, +} + +impl Default for SiteConfig { + fn default() -> Self { + Self { + title: "Narlblog".to_string(), + favicon: "/favicon.svg".to_string(), + theme: "catppuccin-mocha".to_string(), + } + } +} + #[derive(Serialize)] struct PostInfo { slug: String, @@ -30,6 +47,12 @@ struct PostDetail { content: String, } +#[derive(Deserialize)] +struct CreatePostRequest { + slug: String, + content: String, +} + #[derive(Serialize)] struct ErrorResponse { error: String, @@ -67,7 +90,8 @@ async fn main() { .allow_headers(Any); let app = Router::new() - .route("/api/posts", get(list_posts)) + .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/upload", post(upload_file)) .nest_service("/uploads", ServeDir::new(uploads_dir)) @@ -81,6 +105,73 @@ async fn main() { axum::serve(listener, app).await.unwrap(); } +fn check_auth(headers: &HeaderMap, admin_token: &str) -> Result<(), (StatusCode, Json)> { + let auth_header = headers.get("Authorization").and_then(|h| h.to_str().ok()); + if auth_header != Some(&format!("Bearer {}", admin_token)) { + return Err(( + StatusCode::UNAUTHORIZED, + Json(ErrorResponse { error: "Unauthorized".to_string() }), + )); + } + Ok(()) +} + +async fn get_config(State(state): State>) -> impl IntoResponse { + let config_path = state.data_dir.join("config.json"); + let config = fs::read_to_string(&config_path) + .ok() + .and_then(|c| serde_json::from_str::(&c).ok()) + .unwrap_or_default(); + + Json(config) +} + +async fn update_config( + State(state): State>, + headers: HeaderMap, + Json(payload): Json, +) -> Result, (StatusCode, Json)> { + check_auth(&headers, &state.admin_token)?; + + let config_path = state.data_dir.join("config.json"); + let config_str = serde_json::to_string_pretty(&payload).map_err(|_| { + (StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse { error: "Serialization error".to_string() })) + })?; + + fs::write(&config_path, config_str).map_err(|_| { + (StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse { error: "Write error".to_string() })) + })?; + + Ok(Json(payload)) +} + +async fn create_post( + State(state): State>, + headers: HeaderMap, + Json(payload): Json, +) -> Result, (StatusCode, Json)> { + check_auth(&headers, &state.admin_token)?; + + // Validate slug to prevent directory traversal + if payload.slug.contains('/') || payload.slug.contains('\\') || payload.slug.contains("..") { + return Err(( + StatusCode::BAD_REQUEST, + Json(ErrorResponse { error: "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(|_| { + (StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse { error: "Write error".to_string() })) + })?; + + Ok(Json(PostDetail { + slug: payload.slug, + content: payload.content, + })) +} + async fn list_posts(State(state): State>) -> impl IntoResponse { let posts_dir = state.data_dir.join("posts"); let mut posts = Vec::new(); @@ -129,14 +220,7 @@ async fn upload_file( headers: HeaderMap, mut multipart: Multipart, ) -> Result, (StatusCode, Json)> { - // Basic Auth Check - let auth_header = headers.get("Authorization").and_then(|h| h.to_str().ok()); - if auth_header != Some(&format!("Bearer {}", state.admin_token)) { - return Err(( - StatusCode::UNAUTHORIZED, - Json(ErrorResponse { error: "Unauthorized".to_string() }), - )); - } + 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() })) @@ -174,4 +258,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 78d31c5..2556eca 100644 --- a/frontend/src/layouts/Layout.astro +++ b/frontend/src/layouts/Layout.astro @@ -6,6 +6,24 @@ 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'; + +let siteConfig = { + title: "Narlblog", + favicon: "/favicon.svg", + theme: "catppuccin-mocha" +}; + +try { + const res = await fetch(`${API_URL}/api/config`); + if (res.ok) { + siteConfig = await res.json(); + } +} catch (e) { + console.error("Failed to fetch config:", e); +} + --- @@ -13,9 +31,9 @@ const { title } = Astro.props; - + - {title} | Narlblog + {title} | {siteConfig.title}
@@ -23,11 +41,12 @@ const { title } = Astro.props; @@ -37,7 +56,7 @@ const { title } = Astro.props;
- © {new Date().getFullYear()} Narlblog. Built with Rust & Astro. + © {new Date().getFullYear()} {siteConfig.title}. Built with Rust & Astro.
diff --git a/frontend/src/pages/admin/editor.astro b/frontend/src/pages/admin/editor.astro new file mode 100644 index 0000000..f5eb42f --- /dev/null +++ b/frontend/src/pages/admin/editor.astro @@ -0,0 +1,182 @@ +--- +import Layout from '../../layouts/Layout.astro'; +--- + + + + + + diff --git a/frontend/src/pages/admin/index.astro b/frontend/src/pages/admin/index.astro new file mode 100644 index 0000000..3d44a63 --- /dev/null +++ b/frontend/src/pages/admin/index.astro @@ -0,0 +1,47 @@ +--- +import Layout from '../../layouts/Layout.astro'; +--- + + + + + + diff --git a/frontend/src/pages/admin/login.astro b/frontend/src/pages/admin/login.astro new file mode 100644 index 0000000..8b46bf5 --- /dev/null +++ b/frontend/src/pages/admin/login.astro @@ -0,0 +1,44 @@ +--- +import Layout from '../../layouts/Layout.astro'; +--- + + +
+
+

Admin Login

+

Enter your admin token to access the dashboard.

+ +
+
+ + +
+ +
+
+
+ + +
diff --git a/frontend/src/pages/admin/settings.astro b/frontend/src/pages/admin/settings.astro new file mode 100644 index 0000000..58b6020 --- /dev/null +++ b/frontend/src/pages/admin/settings.astro @@ -0,0 +1,120 @@ +--- +import Layout from '../../layouts/Layout.astro'; +--- + + + + + + diff --git a/frontend/src/pages/posts/[slug].astro b/frontend/src/pages/posts/[slug].astro index d0fa7ff..3e2a69e 100644 --- a/frontend/src/pages/posts/[slug].astro +++ b/frontend/src/pages/posts/[slug].astro @@ -63,4 +63,32 @@ function formatSlug(slug: string) {
)} + + + +1.5-5.5Z"/> + Edit + +
+ )} + + + {error && ( +
+

{error}

+ Return home +
+ )} + + {post && ( +
+ )} +