common.rs
⎇
Raw
1//! Authenticated-user extractor shared by all protected API routes.
2
3use std::sync::Arc;
4
5use axum::extract::FromRequestParts;
6use axum::http::request::Parts;
7use axum::http::StatusCode;
8
9use crate::auth::parse_session_cookie;
10use crate::db::{RootRow, ShareRow, User};
11use crate::error::{ApiError, AppState};
12
13pub struct AuthUser {
14 pub user: User,
15 pub roots: Vec<RootRow>,
16 /// Present when authenticated via a public share token. The single entry
17 /// in `roots` is the shared item (its path is the share's `target`), so all
18 /// file operations are scoped to it.
19 #[allow(dead_code)]
20 pub share: Option<ShareRow>,
21}
22
23impl<S> FromRequestParts<S> for AuthUser
24where
25 S: HasState + Send + Sync,
26{
27 type Rejection = ApiError;
28
29 async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
30 let state = state.state();
31
32 // 1. Public share token (`?share=<token>` or `X-Share-Token`). Checked
33 // first: a request that explicitly carries a share token is a share
34 // request, even if a session is also present (so a signed-in user
35 // viewing a share link sees the shared scope).
36 if let Some(share_token) = share_token_from_request(parts) {
37 return match state.db.share_by_token(&share_token).await {
38 Some(share) if !share.is_expired() => {
39 let roots = vec![RootRow {
40 id: share.id,
41 path: share.target.clone(),
42 mode: share.mode.clone(),
43 }];
44 let user = User {
45 id: share.creator_id,
46 name: "shared".to_string(),
47 is_admin: false,
48 };
49 Ok(AuthUser {
50 user,
51 roots,
52 share: Some(share),
53 })
54 }
55 Some(_) => Err(ApiError::new(
56 StatusCode::GONE,
57 "this share has expired",
58 )),
59 None => Err(ApiError::new(
60 StatusCode::NOT_FOUND,
61 "share not found",
62 )),
63 };
64 }
65
66 // 2. Signed-in session.
67 if let Some(token) = parse_session_cookie(&parts.headers) {
68 return match state.db.session_user(&token).await {
69 Some(user) => {
70 let roots = state.db.user_roots(user.id).await;
71 Ok(AuthUser {
72 user,
73 roots,
74 share: None,
75 })
76 }
77 None => Err(ApiError::new(
78 StatusCode::UNAUTHORIZED,
79 "session expired, please sign in again",
80 )),
81 };
82 }
83
84 Err(ApiError::new(StatusCode::UNAUTHORIZED, "not signed in"))
85 }
86}
87
88/// Extract the share token from a request, if present: a `?share=<token>`
89/// query param or an `X-Share-Token` header.
90fn share_token_from_request(parts: &Parts) -> Option<String> {
91 if let Some(q) = parts.uri.query() {
92 for pair in q.split('&') {
93 if let Some((k, v)) = pair.split_once('=') {
94 if k == "share" && !v.is_empty() {
95 return Some(v.to_string());
96 }
97 }
98 }
99 }
100 parts
101 .headers
102 .get("x-share-token")
103 .and_then(|v| v.to_str().ok())
104 .filter(|s| !s.is_empty())
105 .map(|s| s.to_string())
106}
107
108/// Lets the extractor pull the concrete state type out of a generic `S`.
109pub trait HasState {
110 fn state(&self) -> &AppState;
111}
112
113impl HasState for Arc<AppState> {
114 fn state(&self) -> &AppState {
115 self
116 }
117}
118
119impl HasState for AppState {
120 fn state(&self) -> &AppState {
121 self
122 }
123}
124