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(StatusCode::UNAUTHORIZED, "not signed in"));
39 };
40 let Some(user) = state.db.session_user(&token).await else {
41 return Err(ApiError::new(
42 StatusCode::UNAUTHORIZED,
43 "session expired, please sign in again",
44 ));
45 };
46
47 let roots = state
48 .db
49 .user_roots(user.id)
50 .await
51 .into_iter()
52 .map(|r| {
53 serde_json::json!({
54 "id": r.id,
55 "name": display_name(&state.root, &r.path),
56 "path": r.path,
57 "mode": r.mode,
58 })
59 })
60 .collect::<Vec<_>>();
61
62 Ok(Json(serde_json::json!({
63 "first_boot": false,
64 "user": {
65 "id": user.id,
66 "name": user.name,
67 "is_admin": user.is_admin,
68 },
69 "roots": roots,
70 "allow_writable_shares": state.db.allow_writable_shares().await,
71 })))
72}
73
74/// Display name for a user root: the folder name, or the root folder's
75/// own name when the user root is the whole root (".").
76fn display_name(server_root: &Path, rel: &str) -> String {
77 let p = Path::new(rel);
78 let name = if rel == "." {
79 server_root.file_name()
80 } else {
81 p.file_name().filter(|_| !p.as_os_str().is_empty())
82 };
83 name.map(|s| s.to_string_lossy().into_owned())
84 .unwrap_or_else(|| rel.to_string())
85}
86
87/// POST /api/auth/setup — create the first admin account.
88/// Only available while no users exist.
89pub async fn setup(
90 State(state): State<Arc<AppState>>,
91 Json(body): Json<CredentialsBody>,
92) -> Result<Response, ApiError> {
93 let name = body.name.trim();
94 if name.is_empty() || name.len() > 64 {
95 return Err(ApiError::new(
96 StatusCode::BAD_REQUEST,
97 "name must be 1–64 characters",
98 ));
99 }
100 if body.password.len() < 8 {
101 return Err(ApiError::new(
102 StatusCode::BAD_REQUEST,
103 "password must be at least 8 characters",
104 ));
105 }
106 if state.db.user_count().await > 0 {
107 return Err(ApiError::new(
108 StatusCode::CONFLICT,
109 "server is already set up",
110 ));
111 }
112
113 let pass_hash = auth::hash_password(&body.password).map_err(|e| {
114 ApiError::new(
115 StatusCode::INTERNAL_SERVER_ERROR,
116 format!("hashing failed: {e}"),
117 )
118 })?;
119 let user = state.db.create_admin(name, &pass_hash).await?;
120
121 let token = auth::random_token();
122 state.db.create_session(user.id, &token).await?;
123
124 let mut res = Json(serde_json::json!({ "ok": true })).into_response();
125 res.headers_mut().insert(
126 header::SET_COOKIE,
127 session_cookie(&token, state.https).parse().unwrap(),
128 );
129 Ok(res)
130}
131
132/// POST /api/auth/login
133pub async fn login(
134 State(state): State<Arc<AppState>>,
135 Json(body): Json<CredentialsBody>,
136) -> Result<Response, ApiError> {
137 let Some(user) = state.db.verify_password(&body.name, &body.password).await else {
138 return Err(ApiError::new(
139 StatusCode::UNAUTHORIZED,
140 "invalid name or password",
141 ));
142 };
143
144 let token = auth::random_token();
145 state.db.create_session(user.id, &token).await?;
146
147 let mut res = Json(serde_json::json!({ "ok": true })).into_response();
148 res.headers_mut().insert(
149 header::SET_COOKIE,
150 session_cookie(&token, state.https).parse().unwrap(),
151 );
152 Ok(res)
153}
154
155/// POST /api/auth/logout
156pub async fn logout(State(state): State<Arc<AppState>>, headers: HeaderMap) -> Response {
157 if let Some(token) = parse_session_cookie(&headers) {
158 let _ = state.db.delete_session(&token).await;
159 }
160 let mut res = Json(serde_json::json!({ "ok": true })).into_response();
161 res.headers_mut().insert(
162 header::SET_COOKIE,
163 clear_session_cookie(state.https).parse().unwrap(),
164 );
165 res
166}
167