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 let hash = match &body.password {
166 Some(pw) => {
167 validate_password(pw)?;
168 Some(hash_password(pw).await?)
169 }
170 None => None,
171 };
172 let pairs = match &body.roots {
173 Some(roots) => Some(validate_roots(&state, roots).await?),
174 None => None,
175 };
176 state
177 .db
178 .update_user(
179 id,
180 hash.as_deref(),
181 body.is_admin,
182 body.active,
183 pairs.as_deref(),
184 )
185 .await?;
186
187 let updated = state.db.find_user_by_id(id).await?.ok_or_else(|| {
188 ApiError::localized(
189 StatusCode::NOT_FOUND,
190 "user not found",
191 "err_user_not_found",
192 )
193 })?;
194 Ok(Json(user_info(&state.db, &state, &updated).await?))
195}
196
197/// DELETE /api/admin/users/{id} — delete a user (not yourself).
198pub async fn delete_user(
199 State(state): State<Arc<AppState>>,
200 admin: AdminGuard,
201 AxumPath(id): AxumPath<i64>,
202) -> Result<Json<OkResp>, ApiError> {
203 if id == admin.user.id {
204 return Err(ApiError::localized(
205 StatusCode::BAD_REQUEST,
206 "you cannot delete your own account",
207 "err_own_delete",
208 ));
209 }
210 let target = state.db.find_user_by_id(id).await?.ok_or_else(|| {
211 ApiError::localized(
212 StatusCode::NOT_FOUND,
213 "user not found",
214 "err_user_not_found",
215 )
216 })?;
217 if target.is_admin && target.active && state.db.count_admins().await? <= 1 {
218 return Err(ApiError::localized(
219 StatusCode::BAD_REQUEST,
220 "cannot delete the last active admin",
221 "err_last_admin_delete",
222 ));
223 }
224 if !state.db.delete_user(id).await? {
225 return Err(ApiError::localized(
226 StatusCode::NOT_FOUND,
227 "user not found",
228 "err_user_not_found",
229 ));
230 }
231 Ok(Json(OkResp { ok: true }))
232}
233
234/// GET /api/admin/settings
235pub async fn get_settings(
236 State(state): State<Arc<AppState>>,
237 _admin: AdminGuard,
238) -> Result<Json<Settings>, ApiError> {
239 Ok(Json(Settings {
240 allow_writable_shares: state.db.allow_writable_shares().await?,
241 }))
242}
243
244/// PUT /api/admin/settings
245pub async fn update_settings(
246 State(state): State<Arc<AppState>>,
247 _admin: AdminGuard,
248 Json(body): Json<Settings>,
249) -> Result<Json<Settings>, ApiError> {
250 state
251 .db
252 .set_allow_writable_shares(body.allow_writable_shares)
253 .await?;
254 Ok(Json(body))
255}
256