feat: implement centralized logging and authentication middleware

Co-authored-by: aider (openai/andrew/openrouter/anthropic/claude-sonnet-4) <aider@aider.chat>
This commit is contained in:
Andrew Phillips
2025-08-13 13:03:13 -03:00
parent 1170c5ee47
commit 50150ce23d
4 changed files with 21 additions and 91 deletions

View File

@@ -16,7 +16,7 @@ mod common;
mod api; mod api;
mod pages; mod pages;
pub use common::{ServerConfig, AppState, logging_middleware}; pub use common::{ServerConfig, AppState, logging_middleware, create_auth_middleware};
pub fn mode_server( pub fn mode_server(
_cmd: &mut Command, _cmd: &mut Command,
@@ -55,21 +55,21 @@ async fn run_server(
}; };
let app = Router::new() let app = Router::new()
// Add API, documentation, and pages routes first
.merge(api::add_routes(Router::new()))
.merge(api::add_docs_routes(Router::new()))
.merge(pages::add_routes(Router::new()))
// Apply state
.with_state(state)
// Add middleware layers (applied in reverse order)
.layer(axum::middleware::from_fn(logging_middleware)) .layer(axum::middleware::from_fn(logging_middleware))
.layer(axum::middleware::from_fn(create_auth_middleware(config.password.clone())))
.layer( .layer(
ServiceBuilder::new() ServiceBuilder::new()
.layer(TraceLayer::new_for_http()) .layer(TraceLayer::new_for_http())
.layer(CorsLayer::permissive()) .layer(CorsLayer::permissive())
); );
// Add API, documentation, and pages routes
let app = api::add_routes(app);
let app = api::add_docs_routes(app);
let app = pages::add_routes(app);
// Apply state to the router after all routes are added
let app = app.with_state(state);
let addr: SocketAddr = if config.address.starts_with('/') || config.address.starts_with("./") { let addr: SocketAddr = if config.address.starts_with('/') || config.address.starts_with("./") {
// Unix socket - not supported by axum directly, fall back to TCP // Unix socket - not supported by axum directly, fall back to TCP
warn!("Unix sockets not yet implemented, falling back to TCP on 127.0.0.1:8080"); warn!("Unix sockets not yet implemented, falling back to TCP on 127.0.0.1:8080");

View File

@@ -1,12 +1,11 @@
use axum::{ use axum::{
extract::{ConnectInfo, Path, Query, State}, extract::{Path, Query, State},
http::{HeaderMap, StatusCode}, http::{StatusCode},
response::{Json, Response, IntoResponse}, response::{Json, Response, IntoResponse},
http::header, http::header,
}; };
use log::warn; use log::warn;
use std::collections::HashMap; use std::collections::HashMap;
use std::net::SocketAddr;
use std::path::PathBuf; use std::path::PathBuf;
use std::str::FromStr; use std::str::FromStr;
use std::io::Read; use std::io::Read;
@@ -14,7 +13,7 @@ use anyhow::{Result, anyhow};
use crate::compression_engine::{CompressionType, get_compression_engine}; use crate::compression_engine::{CompressionType, get_compression_engine};
use crate::db; use crate::db;
use crate::modes::server::common::{AppState, ApiResponse, ItemInfo, TagsQuery, check_auth, ListItemsQuery}; use crate::modes::server::common::{AppState, ApiResponse, ItemInfo, TagsQuery, ListItemsQuery};
#[utoipa::path( #[utoipa::path(
get, get,
@@ -37,13 +36,7 @@ use crate::modes::server::common::{AppState, ApiResponse, ItemInfo, TagsQuery, c
pub async fn handle_list_items( pub async fn handle_list_items(
State(state): State<AppState>, State(state): State<AppState>,
Query(params): Query<ListItemsQuery>, Query(params): Query<ListItemsQuery>,
headers: HeaderMap,
ConnectInfo(addr): ConnectInfo<SocketAddr>,
) -> Result<Json<ApiResponse<Vec<ItemInfo>>>, StatusCode> { ) -> Result<Json<ApiResponse<Vec<ItemInfo>>>, StatusCode> {
if !check_auth(&headers, &state.password) {
warn!("Unauthorized request to /api/item/ from {}", addr);
return Err(StatusCode::UNAUTHORIZED);
}
let mut conn = state.db.lock().await; let mut conn = state.db.lock().await;
@@ -138,13 +131,7 @@ pub async fn handle_list_items(
)] )]
pub async fn handle_post_item( pub async fn handle_post_item(
State(state): State<AppState>, State(state): State<AppState>,
headers: HeaderMap,
ConnectInfo(addr): ConnectInfo<SocketAddr>,
) -> Result<Json<ApiResponse<ItemInfo>>, StatusCode> { ) -> Result<Json<ApiResponse<ItemInfo>>, StatusCode> {
if !check_auth(&headers, &state.password) {
warn!("Unauthorized request to POST /api/item/ from {}", addr);
return Err(StatusCode::UNAUTHORIZED);
}
// This is a simplified implementation // This is a simplified implementation
// In a real implementation, you'd need to properly parse multipart/form-data // In a real implementation, you'd need to properly parse multipart/form-data
@@ -178,17 +165,9 @@ pub async fn handle_post_item(
pub async fn handle_delete_item( pub async fn handle_delete_item(
State(state): State<AppState>, State(state): State<AppState>,
Path(item_id): Path<i64>, Path(item_id): Path<i64>,
headers: HeaderMap,
ConnectInfo(addr): ConnectInfo<SocketAddr>,
) -> Result<Json<ApiResponse<()>>, StatusCode> { ) -> Result<Json<ApiResponse<()>>, StatusCode> {
if !check_auth(&headers, &state.password) {
warn!("Unauthorized request to DELETE /api/item/{} from {}", item_id, addr);
return Err(StatusCode::UNAUTHORIZED);
}
// Validate that item ID is positive to prevent path traversal issues // Validate that item ID is positive to prevent path traversal issues
if item_id <= 0 { if item_id <= 0 {
warn!("Invalid item ID {} from {}", item_id, addr);
return Err(StatusCode::BAD_REQUEST); return Err(StatusCode::BAD_REQUEST);
} }
@@ -198,10 +177,7 @@ pub async fn handle_delete_item(
warn!("Failed to get item {} for deletion: {}", item_id, e); warn!("Failed to get item {} for deletion: {}", item_id, e);
StatusCode::INTERNAL_SERVER_ERROR StatusCode::INTERNAL_SERVER_ERROR
})? { })? {
db::delete_item(&mut *conn, item).map_err(|e| { db::delete_item(&mut *conn, item).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
warn!("Failed to delete item {}: {}", item_id, e);
StatusCode::INTERNAL_SERVER_ERROR
})?;
let response = ApiResponse::<()> { let response = ApiResponse::<()> {
success: true, success: true,
@@ -233,13 +209,7 @@ pub async fn handle_delete_item(
pub async fn handle_get_item_latest( pub async fn handle_get_item_latest(
State(state): State<AppState>, State(state): State<AppState>,
Query(params): Query<TagsQuery>, Query(params): Query<TagsQuery>,
headers: HeaderMap,
ConnectInfo(addr): ConnectInfo<SocketAddr>,
) -> Result<Json<ApiResponse<String>>, StatusCode> { ) -> Result<Json<ApiResponse<String>>, StatusCode> {
if !check_auth(&headers, &state.password) {
warn!("Unauthorized request to /api/item/latest/content from {}", addr);
return Err(StatusCode::UNAUTHORIZED);
}
let mut conn = state.db.lock().await; let mut conn = state.db.lock().await;
@@ -301,17 +271,9 @@ pub async fn handle_get_item_latest(
pub async fn handle_get_item( pub async fn handle_get_item(
State(state): State<AppState>, State(state): State<AppState>,
Path(item_id): Path<i64>, Path(item_id): Path<i64>,
headers: HeaderMap,
ConnectInfo(addr): ConnectInfo<SocketAddr>,
) -> Result<Json<ApiResponse<String>>, StatusCode> { ) -> Result<Json<ApiResponse<String>>, StatusCode> {
if !check_auth(&headers, &state.password) {
warn!("Unauthorized request to /api/item/{}/content from {}", item_id, addr);
return Err(StatusCode::UNAUTHORIZED);
}
// Validate that item ID is positive to prevent path traversal issues // Validate that item ID is positive to prevent path traversal issues
if item_id <= 0 { if item_id <= 0 {
warn!("Invalid item ID {} from {}", item_id, addr);
return Err(StatusCode::BAD_REQUEST); return Err(StatusCode::BAD_REQUEST);
} }
@@ -364,13 +326,7 @@ pub async fn handle_get_item(
pub async fn handle_get_item_latest_content( pub async fn handle_get_item_latest_content(
State(state): State<AppState>, State(state): State<AppState>,
Query(params): Query<TagsQuery>, Query(params): Query<TagsQuery>,
headers: HeaderMap,
ConnectInfo(addr): ConnectInfo<SocketAddr>,
) -> Result<Response, StatusCode> { ) -> Result<Response, StatusCode> {
if !check_auth(&headers, &state.password) {
warn!("Unauthorized request to /api/item/latest/content from {}", addr);
return Err(StatusCode::UNAUTHORIZED);
}
let mut conn = state.db.lock().await; let mut conn = state.db.lock().await;
@@ -427,17 +383,9 @@ pub async fn handle_get_item_latest_content(
pub async fn handle_get_item_content( pub async fn handle_get_item_content(
State(state): State<AppState>, State(state): State<AppState>,
Path(item_id): Path<i64>, Path(item_id): Path<i64>,
headers: HeaderMap,
ConnectInfo(addr): ConnectInfo<SocketAddr>,
) -> Result<Response, StatusCode> { ) -> Result<Response, StatusCode> {
if !check_auth(&headers, &state.password) {
warn!("Unauthorized request to /api/item/{}/content from {}", item_id, addr);
return Err(StatusCode::UNAUTHORIZED);
}
// Validate that item ID is positive to prevent path traversal issues // Validate that item ID is positive to prevent path traversal issues
if item_id <= 0 { if item_id <= 0 {
warn!("Invalid item ID {} from {}", item_id, addr);
return Err(StatusCode::BAD_REQUEST); return Err(StatusCode::BAD_REQUEST);
} }
@@ -539,13 +487,7 @@ async fn get_item_raw_content(item: &db::Item, data_dir: &PathBuf, conn: &mut ru
pub async fn handle_get_item_latest_meta( pub async fn handle_get_item_latest_meta(
State(state): State<AppState>, State(state): State<AppState>,
Query(params): Query<TagsQuery>, Query(params): Query<TagsQuery>,
headers: HeaderMap,
ConnectInfo(addr): ConnectInfo<SocketAddr>,
) -> Result<Json<ApiResponse<HashMap<String, String>>>, StatusCode> { ) -> Result<Json<ApiResponse<HashMap<String, String>>>, StatusCode> {
if !check_auth(&headers, &state.password) {
warn!("Unauthorized request to /api/item/latest/meta from {}", addr);
return Err(StatusCode::UNAUTHORIZED);
}
let mut conn = state.db.lock().await; let mut conn = state.db.lock().await;
@@ -604,13 +546,7 @@ pub async fn handle_get_item_latest_meta(
pub async fn handle_get_item_meta( pub async fn handle_get_item_meta(
State(state): State<AppState>, State(state): State<AppState>,
Path(item_id): Path<i64>, Path(item_id): Path<i64>,
headers: HeaderMap,
ConnectInfo(addr): ConnectInfo<SocketAddr>,
) -> Result<Json<ApiResponse<HashMap<String, String>>>, StatusCode> { ) -> Result<Json<ApiResponse<HashMap<String, String>>>, StatusCode> {
if !check_auth(&headers, &state.password) {
warn!("Unauthorized request to /api/item/{}/meta from {}", item_id, addr);
return Err(StatusCode::UNAUTHORIZED);
}
let mut conn = state.db.lock().await; let mut conn = state.db.lock().await;

View File

@@ -53,8 +53,9 @@ pub fn add_routes(router: Router<AppState>) -> Router<AppState> {
} }
pub fn add_docs_routes(router: Router<AppState>) -> Router<AppState> { pub fn add_docs_routes(router: Router<AppState>) -> Router<AppState> {
router.merge( router
SwaggerUi::new("/swagger-ui") .merge(SwaggerUi::new("/swagger").url("/openapi.json", ApiDoc::openapi()))
.url("/api-docs/openapi.json", ApiDoc::openapi()) .route("/openapi.json", axum::routing::get(|| async {
) axum::Json(ApiDoc::openapi())
}))
} }

View File

@@ -1,12 +1,11 @@
use axum::{ use axum::{
extract::{ConnectInfo, State}, extract::State,
http::{HeaderMap, StatusCode}, http::StatusCode,
response::Json, response::Json,
}; };
use log::warn; use log::warn;
use std::net::SocketAddr;
use crate::modes::server::common::{AppState, ApiResponse, check_auth}; use crate::modes::server::common::{AppState, ApiResponse};
use crate::common::status::{generate_status_info, StatusInfo}; use crate::common::status::{generate_status_info, StatusInfo};
use crate::meta_plugin::MetaPluginType; use crate::meta_plugin::MetaPluginType;
@@ -24,13 +23,7 @@ use crate::meta_plugin::MetaPluginType;
)] )]
pub async fn handle_status( pub async fn handle_status(
State(state): State<AppState>, State(state): State<AppState>,
headers: HeaderMap,
ConnectInfo(addr): ConnectInfo<SocketAddr>,
) -> Result<Json<ApiResponse<StatusInfo>>, StatusCode> { ) -> Result<Json<ApiResponse<StatusInfo>>, StatusCode> {
if !check_auth(&headers, &state.password) {
warn!("Unauthorized request to /api/status from {}", addr);
return Err(StatusCode::UNAUTHORIZED);
}
// Get database path // Get database path
let db_path = state.db.lock().await.path().unwrap_or("unknown").to_string(); let db_path = state.db.lock().await.path().unwrap_or("unknown").to_string();