a fresh view on git forges
0

Configure Feed

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

auth bootstrap

Norbert Melzer (Jun 1, 2026, 8:29 PM +0200) e23ec0c0 06e9d866

+158 -20
+1
rust/Cargo.lock
··· 409 409 "tokio", 410 410 "tokio-stream", 411 411 "tokio-tungstenite", 412 + "tower", 412 413 "tower-service", 413 414 "tracing", 414 415 "tracing-subscriber",
+1
rust/Cargo.toml
··· 16 16 serde_json = { version = "1.0" } 17 17 tokio-stream = "0.1" 18 18 tokio-tungstenite = "0.29" 19 + tower = { version = "0.5", default-features = false } 19 20 tower-service = "0.3" 20 21 tracing = "0.1" 21 22 tracing-subscriber = "0.3"
+3
rust/henka-api/Cargo.toml
··· 19 19 tower-service.workspace = true 20 20 tracing-subscriber.workspace = true 21 21 tracing.workspace = true 22 + 23 + [dev-dependencies] 24 + tower.workspace = true
+153 -20
rust/henka-api/src/main.rs
··· 2 2 3 3 use axum::{ 4 4 Extension, Router, 5 - response::Html, 5 + body::Body, 6 + http::Request, 7 + middleware::{self, Next}, 8 + response::{Html, Response}, 6 9 routing::{MethodFilter, MethodRouter, get, on}, 7 10 }; 8 11 use futures::stream::{BoxStream, StreamExt as _}; 9 - use juniper::{ 10 - EmptyMutation, FieldError, RootNode, graphql_object, graphql_subscription, 12 + use juniper::{EmptyMutation, FieldError, RootNode, graphql_object, graphql_subscription}; 13 + use juniper_axum::{ 14 + extract::JuniperRequest, graphiql, graphql, playground, response::JuniperResponse, ws, 11 15 }; 12 - use juniper_axum::{graphiql, graphql, playground, ws}; 13 16 use juniper_graphql_ws::ConnectionConfig; 14 17 use tokio::{net::TcpListener, time::interval}; 15 18 use tokio_stream::wrappers::IntervalStream; 16 19 20 + const AUTH_TOKEN: &str = "henka-demo-token"; 21 + 22 + #[derive(Clone, Copy, Debug, Default)] 23 + struct Context { 24 + authorized: bool, 25 + } 26 + 27 + impl juniper::Context for Context {} 28 + 17 29 #[derive(Clone, Copy, Debug)] 18 30 struct Query; 19 31 20 - #[graphql_object] 32 + #[graphql_object(context = Context)] 21 33 impl Query { 22 - /// Adds to `a` and `b` numbers. 23 34 fn add(a: i32, b: i32) -> i32 { 24 35 a + b 36 + } 37 + 38 + fn mul(context: &Context, a: i32, b: i32) -> Result<i32, FieldError> { 39 + if !context.authorized { 40 + return Err(FieldError::from("unauthorized")); 41 + } 42 + Ok(a * b) 25 43 } 26 44 } 27 45 ··· 30 48 31 49 type NumberStream = BoxStream<'static, Result<i32, FieldError>>; 32 50 33 - #[graphql_subscription] 51 + #[graphql_subscription(context = Context)] 34 52 impl Subscription { 35 - /// Counts seconds. 36 53 async fn count() -> NumberStream { 37 54 let mut value = 0; 38 55 let stream = IntervalStream::new(interval(Duration::from_secs(1))).map(move |_| { ··· 43 60 } 44 61 } 45 62 46 - type PublicSchema = RootNode<Query, EmptyMutation, Subscription>; 47 - type PrivateSchema = RootNode<Query, EmptyMutation, Subscription>; 63 + type Schema = RootNode<Query, EmptyMutation<Context>, Subscription>; 48 64 49 65 async fn homepage() -> Html<&'static str> { 50 66 "<html><h1>juniper_axum/simple example</h1>\ 51 67 <div>visit <a href=\"/graphiql\">GraphiQL</a></div>\ 52 68 <div>visit <a href=\"/playground\">GraphQL Playground</a></div>\ 53 - </html>" 69 + </html>" 54 70 .into() 55 71 } 56 72 ··· 65 81 66 82 macro_rules! subscription_router { 67 83 ($sub:ty) => { 68 - get(ws::<Arc<$sub>>(ConnectionConfig::new(()))) 84 + get(ws::<Arc<$sub>>(ConnectionConfig::new(Context { 85 + authorized: false, 86 + }))) 69 87 }; 70 88 } 71 89 ··· 77 95 get(playground(graphql_route, subscription_route)) 78 96 } 79 97 98 + async fn auth_middleware(mut request: Request<Body>, next: Next) -> Result<Response, Response> { 99 + let authorized = request 100 + .headers() 101 + .get("authorization") 102 + .map(|v| match dbg!(v.to_str()) { 103 + Ok(s) if s.to_lowercase().starts_with("bearer ") => dbg!(&s[7..]) == AUTH_TOKEN, 104 + Ok(_) => false, 105 + Err(_) => false, 106 + }) 107 + .unwrap_or(false); 108 + 109 + request.extensions_mut().insert(Context { authorized }); 110 + Ok(next.run(request).await) 111 + } 112 + 113 + async fn graphql_handler( 114 + Extension(schema): Extension<Arc<Schema>>, 115 + Extension(context): Extension<Context>, 116 + JuniperRequest(req): JuniperRequest, 117 + ) -> JuniperResponse { 118 + JuniperResponse(req.execute(&*schema, &context).await) 119 + } 120 + 80 121 #[tokio::main] 81 122 async fn main() { 82 123 tracing_subscriber::fmt() 83 124 .with_max_level(tracing::Level::INFO) 84 125 .init(); 85 126 86 - let public_schema = PublicSchema::new(Query, EmptyMutation::new(), Subscription); 87 - let private_schema = PrivateSchema::new(Query, EmptyMutation::new(), Subscription); 127 + let schema = Arc::new(Schema::new( 128 + Query, 129 + EmptyMutation::<Context>::new(), 130 + Subscription, 131 + )); 88 132 89 133 let app = Router::new() 90 - .route("/graphql", schema_router!(PublicSchema)) 91 - .route("/priv/graphql", schema_router!(PrivateSchema)) 92 - .route("/subscriptions", subscription_router!(PublicSchema)) 93 - .route("/priv/subscriptions", subscription_router!(PrivateSchema)) 134 + .route( 135 + "/graphql", 136 + on(MethodFilter::GET.or(MethodFilter::POST), graphql_handler), 137 + ) 138 + .route_layer(middleware::from_fn(auth_middleware)) 139 + .route("/priv/graphql", schema_router!(Schema)) 140 + .route("/subscriptions", subscription_router!(Schema)) 141 + .route("/priv/subscriptions", subscription_router!(Schema)) 94 142 .route("/graphiql", gw("/graphql", "/subscriptions")) 95 143 .route("/priv/graphiql", gw("/priv/graphql", "/priv/subscriptions")) 96 144 .route("/playground", pg("/graphql", "/subscriptions")) ··· 99 147 pg("/priv/graphql", "/priv/subscriptions"), 100 148 ) 101 149 .route("/", get(homepage)) 102 - .layer(Extension(Arc::new(public_schema))) 103 - .layer(Extension(Arc::new(private_schema))); 150 + .layer(Extension(schema)); 104 151 105 152 let addr = SocketAddr::from(([127, 0, 0, 1], 4000)); 106 153 let listener = TcpListener::bind(addr) ··· 110 157 axum::serve(listener, app) 111 158 .await 112 159 .unwrap_or_else(|e| panic!("failed to run `axum::serve`: {e}")); 160 + } 161 + 162 + #[cfg(test)] 163 + mod tests { 164 + use super::*; 165 + use axum::{body::to_bytes, http::StatusCode}; 166 + use serde_json::Value; 167 + use tower::ServiceExt; 168 + 169 + fn build_app() -> Router { 170 + let schema = Arc::new(Schema::new( 171 + Query, 172 + EmptyMutation::<Context>::new(), 173 + Subscription, 174 + )); 175 + 176 + Router::new() 177 + .route( 178 + "/graphql", 179 + on(MethodFilter::GET.or(MethodFilter::POST), graphql_handler), 180 + ) 181 + .route_layer(middleware::from_fn(auth_middleware)) 182 + .layer(Extension(schema)) 183 + } 184 + 185 + #[tokio::test] 186 + async fn test_add_without_auth() { 187 + let (status, body) = query(build_app(), "{ add(a: 1, b: 2) }", None).await; 188 + assert_eq!(status, StatusCode::OK); 189 + assert_eq!(body["data"]["add"], 3); 190 + } 191 + 192 + #[tokio::test] 193 + async fn test_mul_without_auth_returns_error() { 194 + let (status, body) = query(build_app(), "{ mul(a: 3, b: 4) }", None).await; 195 + assert_eq!(status, StatusCode::OK); 196 + assert!( 197 + body.get("errors").is_some(), 198 + "expected GraphQL error for unauthorized access" 199 + ); 200 + assert!(body["data"]["mul"].is_null()); 201 + } 202 + 203 + #[tokio::test] 204 + async fn test_mul_with_wrong_token_returns_error() { 205 + let (status, body) = query( 206 + build_app(), 207 + "{ mul(a: 3, b: 4) }", 208 + Some("Bearer wrong-token"), 209 + ) 210 + .await; 211 + assert_eq!(status, StatusCode::OK); 212 + assert!( 213 + body.get("errors").is_some(), 214 + "expected GraphQL error for wrong token" 215 + ); 216 + } 217 + 218 + #[tokio::test] 219 + async fn test_mul_with_valid_token() { 220 + let (status, body) = query(build_app(), "{ mul(a: 3, b: 4) }", Some(AUTH_TOKEN)).await; 221 + assert_eq!(status, StatusCode::OK); 222 + assert_eq!(body["data"]["mul"], 12); 223 + } 224 + 225 + async fn query(app: Router, q: &str, token: Option<&str>) -> (StatusCode, Value) { 226 + let mut req_builder = axum::http::Request::builder() 227 + .method("POST") 228 + .uri("/graphql") 229 + .header("content-type", "application/json"); 230 + 231 + if let Some(token) = token { 232 + let bearer = format!("bearer {}", token); 233 + req_builder = req_builder.header("authorization", bearer); 234 + } 235 + 236 + let req = req_builder 237 + .body(Body::from(serde_json::json!({"query": q}).to_string())) 238 + .unwrap(); 239 + 240 + let res = app.oneshot(req).await.unwrap(); 241 + let status = res.status(); 242 + let bytes = to_bytes(res.into_body(), usize::MAX).await.unwrap(); 243 + let body: Value = serde_json::from_slice(&bytes).unwrap(); 244 + (status, body) 245 + } 113 246 }