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(StatusCode::BAD_REQUEST, format!("root path '{path}': {e}"))
124 })?;
125 out.push((path, r.mode.clone()));
126 }
127 Ok(out)
128}
129
130fn validate_name(name: &str) -> Result<(), ApiError> {
131 let n = name.trim();
132 if n.is_empty() || n.len() > 64 {
133 return Err(ApiError::new(
134 StatusCode::BAD_REQUEST,
135 "name must be 1–64 characters",
136 ));
137 }
138 Ok(())
139}
140
141fn validate_password(pw: &str) -> Result<(), ApiError> {
142 if pw.len() < 8 {
143 return Err(ApiError::new(
144 StatusCode::BAD_REQUEST,
145 "password must be at least 8 characters",
146 ));
147 }
148 Ok(())
149}
150
151// ---------------------------------------------------------------------------
152// Handlers
153// ---------------------------------------------------------------------------
154
155/// GET /api/admin/users — list all users with their roots.
156pub async fn list_users(
157 State(state): State<Arc<AppState>>,
158 _admin: AdminUser,
159) -> Result<Json<serde_json::Value>, ApiError> {
160 let users = state.db.all_users().await;
161 let mut values = Vec::with_capacity(users.len());
162 for u in &users {
163 values.push(user_json(&state.db, &state, u).await);
164 }
165 Ok(Json(serde_json::json!(values)))
166}
167
168/// POST /api/admin/users — create a user.
169pub async fn create_user(
170 State(state): State<Arc<AppState>>,
171 _admin: AdminUser,
172 Json(body): Json<CreateUserBody>,
173) -> Result<Json<serde_json::Value>, ApiError> {
174 let name = body.name.trim().to_string();
175 validate_name(&name)?;
176 validate_password(&body.password)?;
177 if state.db.find_user_by_name(&name).await.is_some() {
178 return Err(ApiError::new(
179 StatusCode::CONFLICT,
180 "a user with that name already exists",
181 ));
182 }
183 let roots = validate_roots(&state, &body.roots).await?;
184
185 let pass_hash = auth::hash_password(&body.password).map_err(|e| {
186 ApiError::new(
187 StatusCode::INTERNAL_SERVER_ERROR,
188 format!("hashing failed: {e}"),
189 )
190 })?;
191 let user = state
192 .db
193 .create_user(&name, &pass_hash, body.is_admin, &roots)
194 .await?;
195 Ok(Json(user_json(&state.db, &state, &user).await))
196}
197
198/// PUT /api/admin/users/{id} — update a user (password / is_admin / active /
199/// roots; all optional).
200pub async fn update_user(
201 State(state): State<Arc<AppState>>,
202 admin: AdminUser,
203 AxumPath(id): AxumPath<i64>,
204 Json(body): Json<UpdateUserBody>,
205) -> Result<Json<serde_json::Value>, ApiError> {
206 let target = state
207 .db
208 .find_user_by_id(id)
209 .await
210 .ok_or_else(|| ApiError::new(StatusCode::NOT_FOUND, "user not found"))?;
211
212 // Lockout guards: an admin cannot demote, disable, or delete themselves.
213 if id == admin.user.id {
214 if body.is_admin == Some(false) {
215 return Err(ApiError::new(
216 StatusCode::BAD_REQUEST,
217 "you cannot remove your own admin rights",
218 ));
219 }
220 if body.active == Some(false) {
221 return Err(ApiError::new(
222 StatusCode::BAD_REQUEST,
223 "you cannot disable your own account",
224 ));
225 }
226 }
227 // Never allow dropping to zero active admins.
228 let demoting = id != admin.user.id && body.is_admin == Some(false) && target.is_admin;
229 let disabling =
230 id != admin.user.id && body.active == Some(false) && target.active && target.is_admin;
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).map_err(|e| {
241 ApiError::new(
242 StatusCode::INTERNAL_SERVER_ERROR,
243 format!("hashing failed: {e}"),
244 )
245 })?;
246 state.db.update_user_password(id, &hash).await?;
247 }
248 if let Some(is_admin) = body.is_admin {
249 state.db.set_user_admin(id, is_admin).await?;
250 }
251 if let Some(active) = body.active {
252 state.db.set_user_active(id, active).await?;
253 }
254 if let Some(roots) = &body.roots {
255 let pairs = validate_roots(&state, roots).await?;
256 state.db.set_user_roots(id, &pairs).await?;
257 }
258
259 let updated = state
260 .db
261 .find_user_by_id(id)
262 .await
263 .ok_or_else(|| ApiError::new(StatusCode::NOT_FOUND, "user not found"))?;
264 Ok(Json(user_json(&state.db, &state, &updated).await))
265}
266
267/// DELETE /api/admin/users/{id} — delete a user (not yourself).
268pub async fn delete_user(
269 State(state): State<Arc<AppState>>,
270 admin: AdminUser,
271 AxumPath(id): AxumPath<i64>,
272) -> Result<Json<serde_json::Value>, ApiError> {
273 if id == admin.user.id {
274 return Err(ApiError::new(
275 StatusCode::BAD_REQUEST,
276 "you cannot delete your own account",
277 ));
278 }
279 let target = state
280 .db
281 .find_user_by_id(id)
282 .await
283 .ok_or_else(|| ApiError::new(StatusCode::NOT_FOUND, "user not found"))?;
284 if target.is_admin && target.active && state.db.count_admins().await <= 1 {
285 return Err(ApiError::new(
286 StatusCode::BAD_REQUEST,
287 "cannot delete the last active admin",
288 ));
289 }
290 if !state.db.delete_user(id).await {
291 return Err(ApiError::new(StatusCode::NOT_FOUND, "user not found"));
292 }
293 Ok(Json(serde_json::json!({ "ok": true })))
294}
295
296/// GET /api/admin/settings
297pub async fn get_settings(
298 State(state): State<Arc<AppState>>,
299 _admin: AdminUser,
300) -> Result<Json<serde_json::Value>, ApiError> {
301 Ok(Json(serde_json::json!({
302 "allow_writable_shares": state.db.allow_writable_shares().await,
303 })))
304}
305
306/// PUT /api/admin/settings
307pub async fn update_settings(
308 State(state): State<Arc<AppState>>,
309 _admin: AdminUser,
310 Json(body): Json<SettingsBody>,
311) -> Result<Json<serde_json::Value>, ApiError> {
312 state
313 .db
314 .set_allow_writable_shares(body.allow_writable_shares)
315 .await?;
316 Ok(Json(serde_json::json!({
317 "allow_writable_shares": body.allow_writable_shares,
318 })))
319}
320