• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

divviup / divviup-api / 24687304870

20 Apr 2026 07:57PM UTC coverage: 57.188% (-0.9%) from 58.079%
24687304870

push

github

web-flow
Migrate from Trillium [part 4]: Axum extractors (#2216)

Add `FromRequestParts` impls alongside every existing `FromConn` impl so
that Axum handlers (migrated in Parts 5-7) can extract the same types.
Both impls coexist on the same types; no routes migrate in this PR.

Extractors added:
- Db, Auth0Client, Crypter, FeatureFlags: via `FromRef<AxumAppState>`
- AccountBearerToken: from `Authorization: Bearer` header + DB lookup,
  cached in request extensions
- User: from tower-sessions Session (n.b. this is dead until Part 6 wires
  up `SessionManagerLayer`)
- PermissionsActor: tries bearer token, falls back to session user +
  memberships query, cached in request extensions
- Account, Task, Aggregator, ApiToken, CollectorCredential: via a shared
  `extract_entity` helper that generically handles path param parsing,
  DB lookup, and permission checking

Other changes:
- Expand AxumAppState with auth0_client, crypter, feature_flags fields
- Add `is_allowed_http`/`if_allowed_http` methods on PermissionsActor
  for `http::Method` (this is refactored to share logic with the Trillium
  variants via `check_permission`)
- Enable `axum-core` feature on tower-sessions-core for Session's
  `FromRequestParts` impl

8 of 134 new or added lines in 10 files covered. (5.97%)

1 existing line in 1 file now uncovered.

4344 of 7596 relevant lines covered (57.19%)

60.93 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

64.29
/src/user.rs
1
use crate::{
2
    entity::{AccountColumn, Accounts, MembershipColumn, Memberships},
3
    handler::Error,
4
    Db,
5
};
6
use axum::extract::FromRef;
7
use axum::http::request::Parts;
8
use sea_orm::{
9
    sea_query::all, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QuerySelect, Select,
10
};
11
use serde::{Deserialize, Serialize};
12
use time::{Duration, OffsetDateTime};
13
use tower_sessions_core::Session;
14
use trillium::{async_trait, Conn};
15
use trillium_api::FromConn;
16
use trillium_sessions::SessionConnExt;
17

18
pub const USER_SESSION_KEY: &str = "user";
19

20
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
21
pub struct User {
22
    pub email: String,
23
    pub email_verified: bool,
24
    pub name: String,
25
    pub nickname: String,
26
    pub picture: Option<String>,
27
    pub sub: String,
28
    #[serde(with = "time::serde::rfc3339")]
29
    pub updated_at: OffsetDateTime,
30
    pub admin: Option<bool>,
31
}
32

33
impl User {
34
    pub fn memberships(&self) -> Select<Memberships> {
149✔
35
        Memberships::for_user(self)
149✔
36
    }
149✔
37

38
    pub fn for_integration_testing() -> Self {
276✔
39
        Self {
276✔
40
            email: "integration@test.example".into(),
276✔
41
            email_verified: true,
276✔
42
            name: "integration testing user".into(),
276✔
43
            nickname: "test".into(),
276✔
44
            picture: None,
276✔
45
            sub: "".into(),
276✔
46
            updated_at: OffsetDateTime::now_utc() - Duration::days(1),
276✔
47
            admin: Some(true),
276✔
48
        }
276✔
49
    }
276✔
50

51
    async fn fetch_admin(&self, db: &Db) -> bool {
149✔
52
        Memberships::find()
149✔
53
            .inner_join(Accounts)
149✔
54
            .filter(all![
149✔
55
                MembershipColumn::UserEmail.eq(self.email.clone()),
149✔
56
                AccountColumn::Admin.eq(true)
149✔
57
            ])
149✔
58
            .limit(1)
149✔
59
            .count(db)
149✔
60
            .await
149✔
61
            .ok()
149✔
62
            .is_some_and(|n| n > 0)
149✔
63
    }
149✔
64

65
    async fn populate_admin(&mut self, db: &Db) {
153✔
66
        if self.admin.is_none() {
153✔
67
            self.admin = Some(self.fetch_admin(db).await);
149✔
68
        }
4✔
69
    }
153✔
70

71
    pub fn is_admin(&self) -> bool {
125✔
72
        self.admin == Some(true)
125✔
73
    }
125✔
74
}
75

76
#[async_trait]
77
impl FromConn for User {
78
    async fn from_conn(conn: &mut Conn) -> Option<Self> {
544✔
79
        let mut user: Self = conn
272✔
80
            .take_state()
272✔
81
            .or_else(|| conn.session().get(USER_SESSION_KEY))?;
272✔
82
        let db: &Db = conn.state()?;
153✔
83
        user.populate_admin(db).await;
153✔
84
        conn.insert_state(user.clone());
153✔
85
        Some(user)
153✔
86
    }
544✔
87
}
88

89
// ---------------------------------------------------------------------------
90
// Axum extractor — mirrors the Trillium FromConn above
91
// ---------------------------------------------------------------------------
92
//
93
// Dead until Part 6 wires SessionManagerLayer onto the Axum router.
94
// The tower-sessions Session is placed in request extensions by that layer;
95
// without it, extraction will fail at runtime (which is fine — nothing calls
96
// this until Part 6).
97

98
impl User {
NEW
99
    pub(crate) async fn from_parts<S>(parts: &mut Parts, state: &S) -> Result<Self, Error>
×
NEW
100
    where
×
NEW
101
        Db: FromRef<S>,
×
NEW
102
        S: Send + Sync,
×
NEW
103
    {
×
104
        // Cache: return early if already extracted for this request.
NEW
105
        if let Some(user) = parts.extensions.get::<Self>() {
×
NEW
106
            return Ok(user.clone());
×
NEW
107
        }
×
108

NEW
109
        let session = parts
×
NEW
110
            .extensions
×
NEW
111
            .get::<Session>()
×
NEW
112
            .ok_or(Error::AccessDenied)?;
×
113

NEW
114
        let mut user: Self = session
×
NEW
115
            .get(USER_SESSION_KEY)
×
NEW
116
            .await
×
NEW
117
            .map_err(|e| {
×
NEW
118
                log::error!("session store error: {e}");
×
NEW
119
                Error::String("session store error")
×
NEW
120
            })?
×
NEW
121
            .ok_or(Error::AccessDenied)?;
×
122

NEW
123
        let db = Db::from_ref(state);
×
NEW
124
        user.populate_admin(&db).await;
×
NEW
125
        parts.extensions.insert(user.clone());
×
NEW
126
        Ok(user)
×
NEW
127
    }
×
128
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc