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