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