diff --git a/AGENTS.md b/AGENTS.md index 930e7d9..e7100cc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -50,6 +50,9 @@ pnpm test # Type check without building pnpm check + +# Lint +pnpm lint ``` ## Project Structure diff --git a/crates/server/src/lib.rs b/crates/server/src/lib.rs index fed99f9..fb70ae7 100644 --- a/crates/server/src/lib.rs +++ b/crates/server/src/lib.rs @@ -46,15 +46,19 @@ pub async fn start() -> malfestio_core::Result<()> { .route("/cards", post(api::card::create_card)) .layer(axum_middleware::from_fn(middleware::auth::auth_middleware)); + let optional_auth_routes = Router::new() + .route("/decks", get(api::deck::list_decks)) + .route("/decks/{id}", get(api::deck::get_deck)) + .route("/decks/{id}/cards", get(api::card::list_cards)) + .route("/notes", get(api::note::list_notes)) + .route("/notes/{id}", get(api::note::get_note)) + .layer(axum_middleware::from_fn(middleware::auth::optional_auth_middleware)); + let app = Router::new() .route("/health", get(health_check)) .route("/api/auth/login", post(api::auth::login)) - .route("/api/decks", get(api::deck::list_decks)) - .route("/api/decks/{id}", get(api::deck::get_deck)) - .route("/api/decks/{id}/cards", get(api::card::list_cards)) - .route("/api/notes", get(api::note::list_notes)) - .route("/api/notes/{id}", get(api::note::get_note)) .route("/api/import/article", post(api::importer::import_article)) + .nest("/api", optional_auth_routes) .nest("/api", auth_routes) .layer(TraceLayer::new_for_http()) .layer( diff --git a/crates/server/src/middleware/auth.rs b/crates/server/src/middleware/auth.rs index 426092c..4a87e34 100644 --- a/crates/server/src/middleware/auth.rs +++ b/crates/server/src/middleware/auth.rs @@ -53,3 +53,39 @@ pub async fn auth_middleware(mut req: Request, next: Next) -> Response { .into_response(), } } + +/// Optional auth middleware - populates UserContext if valid token is present, +/// but continues without error if no token or invalid token. +/// +/// Used by endpoints that need to check permissions but don't require authentication. +pub async fn optional_auth_middleware(mut req: Request, next: Next) -> Response { + let auth_header = req.headers().get(http::header::AUTHORIZATION); + + let token = match auth_header.and_then(|h| h.to_str().ok()) { + Some(header_val) if header_val.starts_with("Bearer ") => &header_val[7..], + _ => { + return next.run(req).await; + } + }; + + let client = reqwest::Client::new(); + let pds_url = std::env::var("PDS_URL").unwrap_or_else(|_| "https://bsky.social".to_string()); + + match client + .get(format!("{}/xrpc/com.atproto.server.getSession", pds_url)) + .header("Authorization", format!("Bearer {}", token)) + .send() + .await + { + Ok(response) if response.status().is_success() => { + let body: serde_json::Value = response.json().await.unwrap_or_default(); + let did = body["did"].as_str().unwrap_or("").to_string(); + let handle = body["handle"].as_str().unwrap_or("").to_string(); + + req.extensions_mut().insert(UserContext { did, handle }); + } + _ => {} + } + + next.run(req).await +} diff --git a/web/src/App.tsx b/web/src/App.tsx index 2834791..c1e6d1d 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -27,6 +27,7 @@ const App: Component = () => { } /> + } /> } /> } /> } />