Jump to…
snowinitial commitqoxwzsukwmkx1mo
1//! Shared application state and request-scoped context.
2
3use std::sync::Arc;
4
5use axum::extract::FromRequestParts;
6use axum::http::request::Parts;
7use df_auth::Oidc;
8use df_db::models::User;
9use df_db::PgPool;
10
11use crate::config::Config;
12
13pub struct Inner {
14 pub db: PgPool,
15 pub oidc: Oidc,
16 pub config: Config,
17 /// Repository storage, behind the trait so nothing here knows it is Git.
18 pub store: Arc<dyn df_store::RepoStore>,
19 /// Per-identity rate and concurrency limits (spec §9).
20 pub limiter: crate::ratelimit::Limiter,
21}
22
23#[derive(Clone)]
24pub struct AppState(pub Arc<Inner>);
25
26impl std::ops::Deref for AppState {
27 type Target = Inner;
28 fn deref(&self) -> &Inner {
29 &self.0
30 }
31}
32
33/// The signed-in user, resolved once per request by the session middleware and
34/// stashed in request extensions.
35///
36/// Handlers take `CurrentUser` (optional) or `RequireUser` (enforced) rather
37/// than reading cookies themselves.
38#[derive(Clone)]
39pub struct CurrentUser(pub Option<Arc<User>>);
40
41impl<S: Send + Sync> FromRequestParts<S> for CurrentUser {
42 type Rejection = std::convert::Infallible;
43
44 async fn from_request_parts(parts: &mut Parts, _: &S) -> Result<Self, Self::Rejection> {
45 Ok(parts
46 .extensions
47 .get::<CurrentUser>()
48 .cloned()
49 .unwrap_or(CurrentUser(None)))
50 }
51}
52
53/// The per-request CSRF token, minted or echoed by middleware.
54#[derive(Clone)]
55pub struct CsrfToken(pub String);
56
57impl<S: Send + Sync> FromRequestParts<S> for CsrfToken {
58 type Rejection = std::convert::Infallible;
59
60 async fn from_request_parts(parts: &mut Parts, _: &S) -> Result<Self, Self::Rejection> {
61 Ok(parts
62 .extensions
63 .get::<CsrfToken>()
64 .cloned()
65 .unwrap_or_else(|| CsrfToken(String::new())))
66 }
67}
68
69/// The connecting peer's address, when one is known.
70///
71/// `ConnectInfo<SocketAddr>` itself is not an optional extractor, so a handler
72/// that took it directly would 500 on any request that arrived without it —
73/// which is every request in a test, and any request at all if the server is
74/// ever served without `into_make_service_with_connect_info`. This wrapper makes
75/// absence a value the handler decides about rather than an error.
76#[derive(Clone, Copy, Debug)]
77pub struct PeerAddr(pub Option<std::net::SocketAddr>);
78
79impl PeerAddr {
80 /// Whether the peer is provably local. Absence is not local.
81 pub fn is_loopback(&self) -> bool {
82 self.0.is_some_and(|a| a.ip().is_loopback())
83 }
84
85 pub fn ip(&self) -> Option<std::net::IpAddr> {
86 self.0.map(|a| a.ip())
87 }
88}
89
90impl<S: Send + Sync> FromRequestParts<S> for PeerAddr {
91 type Rejection = std::convert::Infallible;
92
93 async fn from_request_parts(parts: &mut Parts, _: &S) -> Result<Self, Self::Rejection> {
94 Ok(PeerAddr(
95 parts
96 .extensions
97 .get::<axum::extract::ConnectInfo<std::net::SocketAddr>>()
98 .map(|c| c.0),
99 ))
100 }
101}
102
103/// Per-request CSP nonce.
104#[derive(Clone)]
105pub struct Nonce(pub String);
106
107impl<S: Send + Sync> FromRequestParts<S> for Nonce {
108 type Rejection = std::convert::Infallible;
109
110 async fn from_request_parts(parts: &mut Parts, _: &S) -> Result<Self, Self::Rejection> {
111 Ok(parts
112 .extensions
113 .get::<Nonce>()
114 .cloned()
115 .unwrap_or_else(|| Nonce(String::new())))
116 }
117}

117 lines · Rust