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 api_types::{AdminUser, CreateUser, Mode, OkResp, Root, RootInfo, Settings, UpdateUser};
7use axum::Json;
8use axum::extract::{Path as AxumPath, State};
9use axum::http::StatusCode;
10
11use crate::api::common::AdminUser as AdminGuard;
12use crate::api::common::{display_name, hash_password, validate_name, validate_password};
13use crate::db::Db;
14use crate::error::{ApiError, AppState};
15use crate::fs;
16
17// ---------------------------------------------------------------------------
18// Helpers
19// ---------------------------------------------------------------------------
20
21fn root_info(state: &AppState, r: &crate::db::RootRow) -> RootInfo {
22 RootInfo {
23 id: r.id,
24 name: display_name(&state.root, &r.path),
25 path: r.path.clone(),
26 mode: r.mode,
27 }
28}
29
30async fn user_info(db: &Db, state: &AppState, user: &crate::db::User) -> AdminUser {
31 let roots = db.user_roots(user.id).await;
32 AdminUser {
33 id: user.id,
34 name: user.name.clone(),
35 is_admin: user.is_admin,
36 active: user.active,
37 roots: roots.iter().map(|r| root_info(state, r)).collect(),
38 }
39}
40
41/// Validate each requested root path (must exist, be a directory, and stay
42/// inside the server root). Returns the (path, mode) pairs.
43///
44/// The mode needs no check: `Mode` only deserializes from "rw" or "ro", so a
45/// bad value is rejected by the `Json` extractor before this runs.
46async fn validate_roots(state: &AppState, roots: &[Root]) -> Result<Vec<(String, Mode)>, ApiError> {
47 let mut out = Vec::new();
48 for r in roots {
49 let path = if r.path.trim().is_empty() {
50 ".".to_string()
51 } else {
52 r.path.trim().to_string()
53 };
54 let server_root = state.root.clone();
55 let path2 = path.clone();
56 tokio::task::spawn_blocking(move || fs::resolve_root(&server_root, &path2))
57 .await
58 .map_err(|_| ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, "internal error"))?
59 .map_err(|e| {
60 ApiError::new(StatusCode::BAD_REQUEST, format!("root path '{path}': {e}"))
61 })?;
62 out.push((path, r.mode));
63 }
64 Ok(out)
65}
66
67// ---------------------------------------------------------------------------
68// Handlers
69// ---------------------------------------------------------------------------
70
71/// GET /api/admin/users — list all users with their roots.
72pub async fn list_users(
73 State(state): State<Arc<AppState>>,
74 _admin: AdminGuard,
75) -> Result<Json<Vec<AdminUser>>, ApiError> {
76 let users = state.db.all_users().await;
77 let mut out = Vec::with_capacity(users.len());
78 for u in &users {
79 out.push(user_info(&state.db, &state, u).await);
80 }
81 Ok(Json(out))
82}
83
84/// POST /api/admin/users — create a user.
85pub async fn create_user(
86 State(state): State<Arc<AppState>>,
87 _admin: AdminGuard,
88 Json(body): Json<CreateUser>,
89) -> Result<Json<AdminUser>, ApiError> {
90 let name = body.name.trim().to_string();
91 validate_name(&name)?;
92 validate_password(&body.password)?;
93 if state.db.find_user_by_name(&name).await.is_some() {
94 return Err(ApiError::new(
95 StatusCode::CONFLICT,
96 "a user with that name already exists",
97 ));
98 }
99 let roots = validate_roots(&state, &body.roots).await?;
100
101 let pass_hash = hash_password(&body.password).await?;
102 let user = state
103 .db
104 .create_user(&name, &pass_hash, body.is_admin, &roots)
105 .await?;
106 Ok(Json(user_info(&state.db, &state, &user).await))
107}
108
109/// PUT /api/admin/users/{id} — update a user (password / is_admin / active /
110/// roots; all optional).
111pub async fn update_user(
112 State(state): State<Arc<AppState>>,
113 admin: AdminGuard,
114 AxumPath(id): AxumPath<i64>,
115 Json(body): Json<UpdateUser>,
116) -> Result<Json<AdminUser>, ApiError> {
117 let target = state
118 .db
119 .find_user_by_id(id)
120 .await
121 .ok_or_else(|| ApiError::new(StatusCode::NOT_FOUND, "user not found"))?;
122
123 // Lockout guards: an admin cannot demote, disable, or delete themselves.
124 if id == admin.user.id {
125 if body.is_admin == Some(false) {
126 return Err(ApiError::new(
127 StatusCode::BAD_REQUEST,
128 "you cannot remove your own admin rights",
129 ));
130 }
131 if body.active == Some(false) {
132 return Err(ApiError::new(
133 StatusCode::BAD_REQUEST,
134 "you cannot disable your own account",
135 ));
136 }
137 }
138 // Never allow dropping to zero active admins.
139 let demoting = id != admin.user.id && body.is_admin == Some(false) && target.is_admin;
140 let disabling =
141 id != admin.user.id && body.active == Some(false) && target.active && target.is_admin;
142 if (demoting || disabling) && state.db.count_admins().await <= 1 {
143 return Err(ApiError::new(
144 StatusCode::BAD_REQUEST,
145 "cannot remove the last active admin",
146 ));
147 }
148
149 if let Some(pw) = &body.password {
150 validate_password(pw)?;
151 let hash = hash_password(pw).await?;
152 state.db.update_user_password(id, &hash).await?;
153 }
154 if let Some(is_admin) = body.is_admin {
155 state.db.set_user_admin(id, is_admin).await?;
156 }
157 if let Some(active) = body.active {
158 state.db.set_user_active(id, active).await?;
159 }
160 if let Some(roots) = &body.roots {
161 let pairs = validate_roots(&state, roots).await?;
162 state.db.set_user_roots(id, &pairs).await?;
163 }
164
165 let updated = state
166 .db
167 .find_user_by_id(id)
168 .await
169 .ok_or_else(|| ApiError::new(StatusCode::NOT_FOUND, "user not found"))?;
170 Ok(Json(user_info(&state.db, &state, &updated).await))
171}
172
173/// DELETE /api/admin/users/{id} — delete a user (not yourself).
174pub async fn delete_user(
175 State(state): State<Arc<AppState>>,
176 admin: AdminGuard,
177 AxumPath(id): AxumPath<i64>,
178) -> Result<Json<OkResp>, ApiError> {
179 if id == admin.user.id {
180 return Err(ApiError::new(
181 StatusCode::BAD_REQUEST,
182 "you cannot delete your own account",
183 ));
184 }
185 let target = state
186 .db
187 .find_user_by_id(id)
188 .await
189 .ok_or_else(|| ApiError::new(StatusCode::NOT_FOUND, "user not found"))?;
190 if target.is_admin && target.active && state.db.count_admins().await <= 1 {
191 return Err(ApiError::new(
192 StatusCode::BAD_REQUEST,
193 "cannot delete the last active admin",
194 ));
195 }
196 if !state.db.delete_user(id).await {
197 return Err(ApiError::new(StatusCode::NOT_FOUND, "user not found"));
198 }
199 Ok(Json(OkResp { ok: true }))
200}
201
202/// GET /api/admin/settings
203pub async fn get_settings(
204 State(state): State<Arc<AppState>>,
205 _admin: AdminGuard,
206) -> Result<Json<Settings>, ApiError> {
207 Ok(Json(Settings {
208 allow_writable_shares: state.db.allow_writable_shares().await,
209 }))
210}
211
212/// PUT /api/admin/settings
213pub async fn update_settings(
214 State(state): State<Arc<AppState>>,
215 _admin: AdminGuard,
216 Json(body): Json<Settings>,
217) -> Result<Json<Settings>, ApiError> {
218 state
219 .db
220 .set_allow_writable_shares(body.allow_writable_shares)
221 .await?;
222 Ok(Json(body))
223}
224