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