auth.rs
⎇
Raw
1use std::path::Path;
2use std::sync::Arc;
3
4use axum::extract::State;
5use axum::http::{header, HeaderMap, StatusCode};
6use axum::response::{IntoResponse, Response};
7use axum::Json;
8use serde::Deserialize;
9
10use crate::auth::{self, clear_session_cookie, parse_session_cookie, session_cookie};
11use crate::error::{ApiError, AppState};
12
13#[derive(Deserialize)]
14pub struct CredentialsBody {
15 pub name: String,
16 pub password: String,
17}
18
19/// GET /api/auth/me
20///
21/// - No users at all → `200 {"first_boot": true}`
22/// - No/invalid session → `401`
23/// - Valid session → user info + visible roots
24pub async fn me(
25 State(state): State<Arc<AppState>>,
26 headers: HeaderMap,
27) -> Result<Json<serde_json::Value>, ApiError> {
28 if state.db.user_count().await == 0 {
29 return Ok(Json(serde_json::json!({
30 "first_boot": true,
31 "user": null,
32 "roots": [],
33 "allow_writable_shares": false
34 })));
35 }
36
37 let Some(token) = parse_session_cookie(&headers) else {
38 return Err(ApiError::new(
39 StatusCode::UNAUTHORIZED,
40 "not signed in",
41 ));
42 };
43 let Some(user) = state.db.session_user(&token).await else {
44 return Err(ApiError::new(
45 StatusCode::UNAUTHORIZED,
46 "session expired, please sign in again",
47 ));
48 };
49
50 let roots = state
51 .db
52 .user_roots(user.id)
53 .await
54 .into_iter()
55 .map(|r| {
56 serde_json::json!({
57 "id": r.id,
58 "name": display_name(&state.root, &r.path),
59 "path": r.path,
60 "mode": r.mode,
61 })
62 })
63 .collect::<Vec<_>>();
64
65 Ok(Json(serde_json::json!({
66 "first_boot": false,
67 "user": {
68 "id": user.id,
69 "name": user.name,
70 "is_admin": user.is_admin,
71 },
72 "roots": roots,
73 "allow_writable_shares": state.db.allow_writable_shares().await,
74 })))
75}
76
77/// Display name for a user root: the folder name, or the root folder's
78/// own name when the user root is the whole root (".").
79fn display_name(server_root: &Path, rel: &str) -> String {
80 let p = Path::new(rel);
81 let name = if rel == "." {
82 server_root.file_name()
83 } else {
84 p.file_name().filter(|_| !p.as_os_str().is_empty())
85 };
86 name.map(|s| s.to_string_lossy().into_owned())
87 .unwrap_or_else(|| rel.to_string())
88}
89
90/// POST /api/auth/setup — create the first admin account.
91/// Only available while no users exist.
92pub async fn setup(
93 State(state): State<Arc<AppState>>,
94 Json(body): Json<CredentialsBody>,
95) -> Result<Response, ApiError> {
96 let name = body.name.trim();
97 if name.is_empty() || name.len() > 64 {
98 return Err(ApiError::new(
99 StatusCode::BAD_REQUEST,
100 "name must be 1–64 characters",
101 ));
102 }
103 if body.password.len() < 8 {
104 return Err(ApiError::new(
105 StatusCode::BAD_REQUEST,
106 "password must be at least 8 characters",
107 ));
108 }
109 if state.db.user_count().await > 0 {
110 return Err(ApiError::new(
111 StatusCode::CONFLICT,
112 "server is already set up",
113 ));
114 }
115
116 let pass_hash = auth::hash_password(&body.password)
117 .map_err(|e| ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, format!("hashing failed: {e}")))?;
118 let user = state.db.create_admin(name, &pass_hash).await?;
119
120 let token = auth::random_token();
121 state.db.create_session(user.id, &token).await?;
122
123 let mut res = Json(serde_json::json!({ "ok": true })).into_response();
124 res.headers_mut()
125 .insert(header::SET_COOKIE, session_cookie(&token, state.https).parse().unwrap());
126 Ok(res)
127}
128
129/// POST /api/auth/login
130pub async fn login(
131 State(state): State<Arc<AppState>>,
132 Json(body): Json<CredentialsBody>,
133) -> Result<Response, ApiError> {
134 let Some(user) = state.db.verify_password(&body.name, &body.password).await else {
135 return Err(ApiError::new(
136 StatusCode::UNAUTHORIZED,
137 "invalid name or password",
138 ));
139 };
140
141 let token = auth::random_token();
142 state.db.create_session(user.id, &token).await?;
143
144 let mut res = Json(serde_json::json!({ "ok": true })).into_response();
145 res.headers_mut()
146 .insert(header::SET_COOKIE, session_cookie(&token, state.https).parse().unwrap());
147 Ok(res)
148}
149
150/// POST /api/auth/logout
151pub async fn logout(
152 State(state): State<Arc<AppState>>,
153 headers: HeaderMap,
154) -> Response {
155 if let Some(token) = parse_session_cookie(&headers) {
156 let _ = state.db.delete_session(&token).await;
157 }
158 let mut res = Json(serde_json::json!({ "ok": true })).into_response();
159 res.headers_mut()
160 .insert(header::SET_COOKIE, clear_session_cookie(state.https).parse().unwrap());
161 res
162}
163