added admin control panel

This commit is contained in:
2026-03-25 12:08:55 +01:00
parent 2da31a2474
commit 311392bffa
7 changed files with 538 additions and 14 deletions

View File

@@ -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<ErrorResponse>)> {
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<Arc<AppState>>) -> 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::<SiteConfig>(&c).ok())
.unwrap_or_default();
Json(config)
}
async fn update_config(
State(state): State<Arc<AppState>>,
headers: HeaderMap,
Json(payload): Json<SiteConfig>,
) -> Result<Json<SiteConfig>, (StatusCode, Json<ErrorResponse>)> {
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<Arc<AppState>>,
headers: HeaderMap,
Json(payload): Json<CreatePostRequest>,
) -> Result<Json<PostDetail>, (StatusCode, Json<ErrorResponse>)> {
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<Arc<AppState>>) -> 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<Json<UploadResponse>, (StatusCode, Json<ErrorResponse>)> {
// 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() }),
))
}
}