admin.rs
⎇
Raw
1//! Admin API (milestone 7): user management and server settings.
2//! All routes require an admin session (via [`AdminUser`]).
3
4use std::sync::Arc;
5
6use axum::extract::{Path as AxumPath, State};
7use axum::http::StatusCode;
8use axum::Json;
9use serde::Deserialize;
10
11use crate::api::common::AdminUser;
12use crate::auth;
13use crate::db::Db;
14use crate::error::{ApiError, AppState};
15use crate::fs;
16
17// ---------------------------------------------------------------------------
18// Bodies
19// ---------------------------------------------------------------------------
20
21#[derive(Deserialize)]
22pub struct RootBody {
23 /// Path relative to the server root; "." means the whole root.
24 path: String,
25 /// "rw" or "ro".
26 #[serde(default = "default_rw")]
27 mode: String,
28}
29
30fn default_rw() -> String {
31 "rw".to_string()
32}
33
34#[derive(Deserialize)]
35pub struct CreateUserBody {
36 name: String,
37 password: String,
38 #[serde(default)]
39 is_admin: bool,
40 #[serde(default)]
41 roots: Vec<RootBody>,
42}
43
44#[derive(Deserialize)]
45pub struct UpdateUserBody {
46 #[serde(default)]
47 password: Option<String>,
48 #[serde(default)]
49 is_admin: Option<bool>,
50 #[serde(default)]
51 active: Option<bool>,
52 #[serde(default)]
53 roots: Option<Vec<RootBody>>,
54}
55
56#[derive(Deserialize)]
57pub struct SettingsBody {
58 allow_writable_shares: bool,
59}
60
61// ---------------------------------------------------------------------------
62// Helpers
63// ---------------------------------------------------------------------------
64
65/// Display name for a root path (folder name, or the server root's name for ".").
66fn display_name(state_root: &std::path::Path, rel: &str) -> String {
67 let name = if rel == "." {
68 state_root.file_name()
69 } else {
70 std::path::Path::new(rel)
71 .file_name()
72 .filter(|_| !std::path::Path::new(rel).as_os_str().is_empty())
73 };
74 name.map(|s| s.to_string_lossy().into_owned())
75 .unwrap_or_else(|| rel.to_string())
76}
77
78fn root_json(state: &AppState, r: &crate::db::RootRow) -> serde_json::Value {
79 serde_json::json!({
80 "id": r.id,
81 "name": display_name(&state.root, &r.path),
82 "path": r.path,
83 "mode": r.mode,
84 })
85}
86
87async fn user_json(db: &Db, state: &AppState, user: &crate::db::User) -> serde_json::Value {
88 let roots = db.user_roots(user.id).await;
89 serde_json::json!({
90 "id": user.id,
91 "name": user.name,
92 "is_admin": user.is_admin,
93 "active": user.active,
94 "roots": roots.iter().map(|r| root_json(state, r)).collect::<Vec<_>>(),
95 })
96}
97
98/// Validate each requested root path (must exist, be a directory, and stay
99/// inside the server root) and its mode. Returns the (path, mode) pairs.
100async fn validate_roots(
101 state: &AppState,
102 roots: &[RootBody],
103) -> Result<Vec<(String, String)>, ApiError> {
104 let mut out = Vec::new();
105 for r in roots {
106 let path = if r.path.trim().is_empty() {
107 ".".to_string()
108 } else {
109 r.path.trim().to_string()
110 };
111 if r.mode != "rw" && r.mode != "ro" {
112 return Err(ApiError::new(
113 StatusCode::BAD_REQUEST,
114 "mode must be 'rw' or 'ro'",
115 ));
116 }
117 let server_root = state.root.clone();
118 let path2 = path.clone();
119 tokio::task::spawn_blocking(move || fs::resolve_root(&server_root, &path2))
120 .await
121 .map_err(|_| ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, "internal error"))?
122 .map_err(|e| {
123 ApiError::new(
124 StatusCode::BAD_REQUEST,
125 format!("root path '{path}': {e}"),
126 )
127 })?;
128 out.push((path, r.mode.clone()));
129 }
130 Ok(out)
131}
132
133fn validate_name(name: &str) -> Result<(), ApiError> {
134 let n = name.trim();
135 if n.is_empty() || n.len() > 64 {
136 return Err(ApiError::new(
137 StatusCode::BAD_REQUEST,
138 "name must be 1–64 characters",
139 ));
140 }
141 Ok(())
142}
143
144fn validate_password(pw: &str) -> Result<(), ApiError> {
145 if pw.len() < 8 {
146 return Err(ApiError::new(
147 StatusCode::BAD_REQUEST,
148 "password must be at least 8 characters",
149 ));
150 }
151 Ok(())
152}
153
154// ---------------------------------------------------------------------------
155// Handlers
156// ---------------------------------------------------------------------------
157
158/// GET /api/admin/users — list all users with their roots.
159pub async fn list_users(
160 State(state): State<Arc<AppState>>,
161 _admin: AdminUser,
162) -> Result<Json<serde_json::Value>, ApiError> {
163 let users = state.db.all_users().await;
164 let mut values = Vec::with_capacity(users.len());
165 for u in &users {
166 values.push(user_json(&state.db, &state, u).await);
167 }
168 Ok(Json(serde_json::json!(values)))
169}
170
171/// POST /api/admin/users — create a user.
172pub async fn create_user(
173 State(state): State<Arc<AppState>>,
174 _admin: AdminUser,
175 Json(body): Json<CreateUserBody>,
176) -> Result<Json<serde_json::Value>, ApiError> {
177 let name = body.name.trim().to_string();
178 validate_name(&name)?;
179 validate_password(&body.password)?;
180 if state.db.find_user_by_name(&name).await.is_some() {
181 return Err(ApiError::new(
182 StatusCode::CONFLICT,
183 "a user with that name already exists",
184 ));
185 }
186 let roots = validate_roots(&state, &body.roots).await?;
187
188 let pass_hash = auth::hash_password(&body.password)
189 .map_err(|e| ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, format!("hashing failed: {e}")))?;
190 let user = state
191 .db
192 .create_user(&name, &pass_hash, body.is_admin, &roots)
193 .await?;
194 Ok(Json(user_json(&state.db, &state, &user).await))
195}
196
197/// PUT /api/admin/users/{id} — update a user (password / is_admin / active /
198/// roots; all optional).
199pub async fn update_user(
200 State(state): State<Arc<AppState>>,
201 admin: AdminUser,
202 AxumPath(id): AxumPath<i64>,
203 Json(body): Json<UpdateUserBody>,
204) -> Result<Json<serde_json::Value>, ApiError> {
205 let target = state
206 .db
207 .find_user_by_id(id)
208 .await
209 .ok_or_else(|| ApiError::new(StatusCode::NOT_FOUND, "user not found"))?;
210
211 // Lockout guards: an admin cannot demote, disable, or delete themselves.
212 if id == admin.user.id {
213 if body.is_admin == Some(false) {
214 return Err(ApiError::new(
215 StatusCode::BAD_REQUEST,
216 "you cannot remove your own admin rights",
217 ));
218 }
219 if body.active == Some(false) {
220 return Err(ApiError::new(
221 StatusCode::BAD_REQUEST,
222 "you cannot disable your own account",
223 ));
224 }
225 }
226 // Never allow dropping to zero active admins.
227 let demoting = id != admin.user.id
228 && body.is_admin == Some(false)
229 && target.is_admin;
230 let disabling = id != admin.user.id && body.active == Some(false) && target.active;
231 if (demoting || disabling) && state.db.count_admins().await <= 1 {
232 return Err(ApiError::new(
233 StatusCode::BAD_REQUEST,
234 "cannot remove the last active admin",
235 ));
236 }
237
238 if let Some(pw) = &body.password {
239 validate_password(pw)?;
240 let hash = auth::hash_password(pw)
241 .map_err(|e| ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, format!("hashing failed: {e}")))?;
242 state.db.update_user_password(id, &hash).await?;
243 }
244 if let Some(is_admin) = body.is_admin {
245 state.db.set_user_admin(id, is_admin).await?;
246 }
247 if let Some(active) = body.active {
248 state.db.set_user_active(id, active).await?;
249 }
250 if let Some(roots) = &body.roots {
251 let pairs = validate_roots(&state, roots).await?;
252 state.db.set_user_roots(id, &pairs).await?;
253 }
254
255 let updated = state
256 .db
257 .find_user_by_id(id)
258 .await
259 .ok_or_else(|| ApiError::new(StatusCode::NOT_FOUND, "user not found"))?;
260 Ok(Json(user_json(&state.db, &state, &updated).await))
261}
262
263/// DELETE /api/admin/users/{id} — delete a user (not yourself).
264pub async fn delete_user(
265 State(state): State<Arc<AppState>>,
266 admin: AdminUser,
267 AxumPath(id): AxumPath<i64>,
268) -> Result<Json<serde_json::Value>, ApiError> {
269 if id == admin.user.id {
270 return Err(ApiError::new(
271 StatusCode::BAD_REQUEST,
272 "you cannot delete your own account",
273 ));
274 }
275 let target = state
276 .db
277 .find_user_by_id(id)
278 .await
279 .ok_or_else(|| ApiError::new(StatusCode::NOT_FOUND, "user not found"))?;
280 if target.is_admin && target.active && state.db.count_admins().await <= 1 {
281 return Err(ApiError::new(
282 StatusCode::BAD_REQUEST,
283 "cannot delete the last active admin",
284 ));
285 }
286 if !state.db.delete_user(id).await {
287 return Err(ApiError::new(StatusCode::NOT_FOUND, "user not found"));
288 }
289 Ok(Json(serde_json::json!({ "ok": true })))
290}
291
292/// GET /api/admin/settings
293pub async fn get_settings(
294 State(state): State<Arc<AppState>>,
295 _admin: AdminUser,
296) -> Result<Json<serde_json::Value>, ApiError> {
297 Ok(Json(serde_json::json!({
298 "allow_writable_shares": state.db.allow_writable_shares().await,
299 })))
300}
301
302/// PUT /api/admin/settings
303pub async fn update_settings(
304 State(state): State<Arc<AppState>>,
305 _admin: AdminUser,
306 Json(body): Json<SettingsBody>,
307) -> Result<Json<serde_json::Value>, ApiError> {
308 state
309 .db
310 .set_allow_writable_shares(body.allow_writable_shares)
311 .await?;
312 Ok(Json(serde_json::json!({
313 "allow_writable_shares": body.allow_writable_shares,
314 })))
315}
316