learn and share notes on atproto (wip) 🦉 malfestio.stormlightlabs.org/
readability solid axum atproto srs
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: implement optional auth middleware

+49 -5
+3
AGENTS.md
··· 50 50 51 51 # Type check without building 52 52 pnpm check 53 + 54 + # Lint 55 + pnpm lint 53 56 ``` 54 57 55 58 ## Project Structure
+9 -5
crates/server/src/lib.rs
··· 46 46 .route("/cards", post(api::card::create_card)) 47 47 .layer(axum_middleware::from_fn(middleware::auth::auth_middleware)); 48 48 49 + let optional_auth_routes = Router::new() 50 + .route("/decks", get(api::deck::list_decks)) 51 + .route("/decks/{id}", get(api::deck::get_deck)) 52 + .route("/decks/{id}/cards", get(api::card::list_cards)) 53 + .route("/notes", get(api::note::list_notes)) 54 + .route("/notes/{id}", get(api::note::get_note)) 55 + .layer(axum_middleware::from_fn(middleware::auth::optional_auth_middleware)); 56 + 49 57 let app = Router::new() 50 58 .route("/health", get(health_check)) 51 59 .route("/api/auth/login", post(api::auth::login)) 52 - .route("/api/decks", get(api::deck::list_decks)) 53 - .route("/api/decks/{id}", get(api::deck::get_deck)) 54 - .route("/api/decks/{id}/cards", get(api::card::list_cards)) 55 - .route("/api/notes", get(api::note::list_notes)) 56 - .route("/api/notes/{id}", get(api::note::get_note)) 57 60 .route("/api/import/article", post(api::importer::import_article)) 61 + .nest("/api", optional_auth_routes) 58 62 .nest("/api", auth_routes) 59 63 .layer(TraceLayer::new_for_http()) 60 64 .layer(
+36
crates/server/src/middleware/auth.rs
··· 53 53 .into_response(), 54 54 } 55 55 } 56 + 57 + /// Optional auth middleware - populates UserContext if valid token is present, 58 + /// but continues without error if no token or invalid token. 59 + /// 60 + /// Used by endpoints that need to check permissions but don't require authentication. 61 + pub async fn optional_auth_middleware(mut req: Request, next: Next) -> Response { 62 + let auth_header = req.headers().get(http::header::AUTHORIZATION); 63 + 64 + let token = match auth_header.and_then(|h| h.to_str().ok()) { 65 + Some(header_val) if header_val.starts_with("Bearer ") => &header_val[7..], 66 + _ => { 67 + return next.run(req).await; 68 + } 69 + }; 70 + 71 + let client = reqwest::Client::new(); 72 + let pds_url = std::env::var("PDS_URL").unwrap_or_else(|_| "https://bsky.social".to_string()); 73 + 74 + match client 75 + .get(format!("{}/xrpc/com.atproto.server.getSession", pds_url)) 76 + .header("Authorization", format!("Bearer {}", token)) 77 + .send() 78 + .await 79 + { 80 + Ok(response) if response.status().is_success() => { 81 + let body: serde_json::Value = response.json().await.unwrap_or_default(); 82 + let did = body["did"].as_str().unwrap_or("").to_string(); 83 + let handle = body["handle"].as_str().unwrap_or("").to_string(); 84 + 85 + req.extensions_mut().insert(UserContext { did, handle }); 86 + } 87 + _ => {} 88 + } 89 + 90 + next.run(req).await 91 + }
+1
web/src/App.tsx
··· 27 27 <Router> 28 28 <Route path="/login" component={Login} /> 29 29 <Route path="/" component={() => <ProtectedRoute component={Home} />} /> 30 + <Route path="/decks" component={() => <ProtectedRoute component={Home} />} /> 30 31 <Route path="/decks/new" component={() => <ProtectedRoute component={DeckNew} />} /> 31 32 <Route path="/notes/new" component={() => <ProtectedRoute component={NoteNew} />} /> 32 33 <Route path="/decks/:id" component={() => <ProtectedRoute component={DeckView} />} />