• 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

0.0
/src/handler/extract.rs
1
/// Shared helper for Axum entity extractors.
2
///
3
/// Each "entity extractor" (Account, Task, Aggregator, ApiToken,
4
/// CollectorCredential) follows the same pattern:
5
///
6
///  1. Extract path parameter by name → parse as UUID
7
///  2. Extract [`PermissionsActor`] (bearer token **or** session user)
8
///  3. Look the entity up by primary key
9
///  4. Check permissions via [`Permissions`] trait
10
///
11
/// [`extract_entity`] captures this pattern as a single generic async
12
/// function, and the per-type [`FromRequestParts`] impls become one-liners.
13
use std::collections::HashMap;
14

15
use axum::extract::{FromRef, FromRequestParts, Path};
16
use axum::http::request::Parts;
17
use sea_orm::EntityTrait;
18
use uuid::Uuid;
19

20
use crate::{handler::Error, Db, Permissions, PermissionsActor};
21

22
/// Look up an entity by a named path parameter, check permissions, and
23
/// return it — or an appropriate [`Error`].
24
///
25
/// `E` is the Sea-ORM **Entity** type (e.g. `Accounts`). Its `Model` must
26
/// implement [`Permissions`] and [`Clone`].
27
///
28
/// **Note**: This always requires a valid [`PermissionsActor`], so it cannot
29
/// be used for truly public (unauthenticated) entity endpoints. If such a
30
/// route is needed in the future, extract the entity and check permissions
31
/// separately.
32
///
33
/// # Errors
34
///
35
/// * [`Error::NotFound`] — path param missing / unparseable, or no DB row
36
/// * [`Error::AccessDenied`] — actor lacks permission for the HTTP method
37
/// * Propagates DB errors and [`PermissionsActor`] extraction failures
NEW
38
pub async fn extract_entity<E, S>(
×
NEW
39
    parts: &mut Parts,
×
NEW
40
    state: &S,
×
NEW
41
    param_name: &str,
×
NEW
42
) -> Result<E::Model, Error>
×
NEW
43
where
×
NEW
44
    E: EntityTrait,
×
NEW
45
    <E::PrimaryKey as sea_orm::PrimaryKeyTrait>::ValueType: From<Uuid>,
×
NEW
46
    E::Model: Permissions + Clone,
×
NEW
47
    Db: FromRef<S>,
×
NEW
48
    S: Send + Sync,
×
NEW
49
{
×
NEW
50
    let Path(params) = Path::<HashMap<String, String>>::from_request_parts(parts, state)
×
NEW
51
        .await
×
NEW
52
        .map_err(|_| Error::NotFound)?;
×
53

NEW
54
    let id = params
×
NEW
55
        .get(param_name)
×
NEW
56
        .and_then(|s| s.parse::<Uuid>().ok())
×
NEW
57
        .ok_or(Error::NotFound)?;
×
58

NEW
59
    let actor = PermissionsActor::from_request_parts(parts, state).await?;
×
NEW
60
    let db = Db::from_ref(state);
×
61

NEW
62
    let entity = E::find_by_id(id).one(&db).await?.ok_or(Error::NotFound)?;
×
63

NEW
64
    actor
×
NEW
65
        .if_allowed_http(&parts.method, entity)
×
NEW
66
        .ok_or(Error::AccessDenied)
×
NEW
67
}
×
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