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.root, &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 users = state.db.all_users().await?;
87 let mut out = Vec::with_capacity(users.len());
88 for u in &users {
89 out.push(user_info(&state.db, &state, u).await?);
90 }
91 Ok(Json(out))
92}
93
94/// POST /api/admin/users — create a user.
95pub async fn create_user(
96 State(state): State<Arc<AppState>>,
97 _admin: AdminGuard,
98 Json(body): Json<CreateUser>,
99) -> Result<Json<AdminUser>, ApiError> {
100 let name = body.name.trim().to_string();
101 validate_account_name(&name)?;
102 validate_password(&body.password)?;
103 if state.db.find_user_by_name(&name).await?.is_some() {
104 return Err(ApiError::localized(
105 StatusCode::CONFLICT,
106 "a user with that name already exists",
107 "err_user_exists",
108 ));
109 }
110 let roots = validate_roots(&state, &body.roots).await?;
111
112 let pass_hash = hash_password(&body.password).await?;
113 let user = state
114 .db
115 .create_user(&name, &pass_hash, body.is_admin, &roots)
116 .await?;
117 Ok(Json(user_info(&state.db, &state, &user).await?))
118}
119
120/// PUT /api/admin/users/{id} — update a user (password / is_admin / active /
121/// roots; all optional).
122pub async fn update_user(
123 State(state): State<Arc<AppState>>,
124 admin: AdminGuard,
125 AxumPath(id): AxumPath<i64>,
126 Json(body): Json<UpdateUser>,
127) -> Result<Json<AdminUser>, ApiError> {
128 let target = state.db.find_user_by_id(id).await?.ok_or_else(|| {
129 ApiError::localized(
130 StatusCode::NOT_FOUND,
131 "user not found",
132 "err_user_not_found",
133 )
134 })?;
135
136 // Lockout guards: an admin cannot demote, disable, or delete themselves.
137 if id == admin.user.id {
138 if body.is_admin == Some(false) {
139 return Err(ApiError::localized(
140 StatusCode::BAD_REQUEST,
141 "you cannot remove your own admin rights",
142 "err_own_admin",
143 ));
144 }
145 if body.active == Some(false) {
146 return Err(ApiError::localized(
147 StatusCode::BAD_REQUEST,
148 "you cannot disable your own account",
149 "err_own_account",
150 ));
151 }
152 }
153 // Never allow dropping to zero active admins.
154 let demoting = id != admin.user.id && body.is_admin == Some(false) && target.is_admin;
155 let disabling =
156 id != admin.user.id && body.active == Some(false) && target.active && target.is_admin;
157 if (demoting || disabling) && state.db.count_admins().await? <= 1 {
158 return Err(ApiError::localized(
159 StatusCode::BAD_REQUEST,
160 "cannot remove the last active admin",
161 "err_last_admin",
162 ));
163 }
164
165 if let Some(pw) = &body.password {
166 validate_password(pw)?;
167 let hash = hash_password(pw).await?;
168 state.db.update_user_password(id, &hash).await?;
169 }
170 if let Some(is_admin) = body.is_admin {
171 state.db.set_user_admin(id, is_admin).await?;
172 }
173 if let Some(active) = body.active {
174 state.db.set_user_active(id, active).await?;
175 }
176 if let Some(roots) = &body.roots {
177 let pairs = validate_roots(&state, roots).await?;
178 state.db.set_user_roots(id, &pairs).await?;
179 }
180
181 let updated = state.db.find_user_by_id(id).await?.ok_or_else(|| {
182 ApiError::localized(
183 StatusCode::NOT_FOUND,
184 "user not found",
185 "err_user_not_found",
186 )
187 })?;
188 Ok(Json(user_info(&state.db, &state, &updated).await?))
189}
190
191/// DELETE /api/admin/users/{id} — delete a user (not yourself).
192pub async fn delete_user(
193 State(state): State<Arc<AppState>>,
194 admin: AdminGuard,
195 AxumPath(id): AxumPath<i64>,
196) -> Result<Json<OkResp>, ApiError> {
197 if id == admin.user.id {
198 return Err(ApiError::localized(
199 StatusCode::BAD_REQUEST,
200 "you cannot delete your own account",
201 "err_own_delete",
202 ));
203 }
204 let target = state.db.find_user_by_id(id).await?.ok_or_else(|| {
205 ApiError::localized(
206 StatusCode::NOT_FOUND,
207 "user not found",
208 "err_user_not_found",
209 )
210 })?;
211 if target.is_admin && target.active && state.db.count_admins().await? <= 1 {
212 return Err(ApiError::localized(
213 StatusCode::BAD_REQUEST,
214 "cannot delete the last active admin",
215 "err_last_admin_delete",
216 ));
217 }
218 if !state.db.delete_user(id).await? {
219 return Err(ApiError::localized(
220 StatusCode::NOT_FOUND,
221 "user not found",
222 "err_user_not_found",
223 ));
224 }
225 Ok(Json(OkResp { ok: true }))
226}
227
228/// GET /api/admin/settings
229pub async fn get_settings(
230 State(state): State<Arc<AppState>>,
231 _admin: AdminGuard,
232) -> Result<Json<Settings>, ApiError> {
233 Ok(Json(Settings {
234 allow_writable_shares: state.db.allow_writable_shares().await?,
235 }))
236}
237
238/// PUT /api/admin/settings
239pub async fn update_settings(
240 State(state): State<Arc<AppState>>,
241 _admin: AdminGuard,
242 Json(body): Json<Settings>,
243) -> Result<Json<Settings>, ApiError> {
244 state
245 .db
246 .set_allow_writable_shares(body.allow_writable_shares)
247 .await?;
248 Ok(Json(body))
249}
250