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 active: true,
49 };
50 Ok(AuthUser {
51 user,
52 roots,
53 share: Some(share),
54 })
55 }
56 Some(_) => Err(ApiError::new(
57 StatusCode::GONE,
58 "this share has expired",
59 )),
60 None => Err(ApiError::new(
61 StatusCode::NOT_FOUND,
62 "share not found",
63 )),
64 };
65 }
66
67 // 2. Signed-in session.
68 if let Some(token) = parse_session_cookie(&parts.headers) {
69 return match state.db.session_user(&token).await {
70 Some(user) => {
71 let roots = state.db.user_roots(user.id).await;
72 Ok(AuthUser {
73 user,
74 roots,
75 share: None,
76 })
77 }
78 None => Err(ApiError::new(
79 StatusCode::UNAUTHORIZED,
80 "session expired, please sign in again",
81 )),
82 };
83 }
84
85 Err(ApiError::new(StatusCode::UNAUTHORIZED, "not signed in"))
86 }
87}
88
89/// Extract the share token from a request, if present: a `?share=<token>`
90/// query param or an `X-Share-Token` header.
91fn share_token_from_request(parts: &Parts) -> Option<String> {
92 if let Some(q) = parts.uri.query() {
93 for pair in q.split('&') {
94 if let Some((k, v)) = pair.split_once('=') {
95 if k == "share" && !v.is_empty() {
96 return Some(v.to_string());
97 }
98 }
99 }
100 }
101 parts
102 .headers
103 .get("x-share-token")
104 .and_then(|v| v.to_str().ok())
105 .filter(|s| !s.is_empty())
106 .map(|s| s.to_string())
107}
108
109/// Lets the extractor pull the concrete state type out of a generic `S`.
110pub trait HasState {
111 fn state(&self) -> &AppState;
112}
113
114impl HasState for Arc<AppState> {
115 fn state(&self) -> &AppState {
116 self
117 }
118}
119
120impl HasState for AppState {
121 fn state(&self) -> &AppState {
122 self
123 }
124}
125
126/// Extractor for admin-only routes: a signed-in user who is an admin.
127pub struct AdminUser {
128 pub user: User,
129}
130
131impl<S> FromRequestParts<S> for AdminUser
132where
133 S: HasState + Send + Sync,
134{
135 type Rejection = ApiError;
136
137 async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
138 let auth = AuthUser::from_request_parts(parts, state).await?;
139 if !auth.user.is_admin {
140 return Err(ApiError::new(StatusCode::FORBIDDEN, "admin only"));
141 }
142 Ok(AdminUser {
143 user: auth.user,
144 })
145 }
146}
147