added admin control panel
This commit is contained in:
@@ -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() }),
|
||||
))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user