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