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