1#![doc = include_str!("../../../doc/EXT_USERS.md")]
4
5use crate::{
6 MyError,
7 db::user::{
8 batch_update_users, find_all_ids, find_group_member_ids, find_group_user, find_user,
9 insert_user, update_user,
10 },
11 emit_response, eval_preconditions,
12 lrs::{
13 DB, Headers, Role, User, etag_from_str, resources::WithResource,
14 server::get_consistent_thru,
15 },
16};
17use chrono::SecondsFormat;
18use rocket::{
19 FromForm, Route, State,
20 form::Form,
21 futures::TryFutureExt,
22 get,
23 http::{Header, Status, hyper::header},
24 post, put, routes,
25 serde::json::Json,
26};
27use tracing::{debug, info};
28use xapi_data::DataError;
29
30#[derive(Debug, FromForm)]
32struct CreateForm<'a> {
33 email: &'a str,
34 password: &'a str,
35 #[field(validate = range(0..4))]
37 role: u16,
38}
39
40#[derive(Debug, FromForm)]
41#[field(validate = range(0..4))]
42pub(crate) struct RoleUI(pub(crate) u16);
43
44#[derive(Debug, FromForm)]
46pub(crate) struct UpdateForm<'a> {
47 pub(crate) enabled: Option<bool>,
48 pub(crate) email: Option<&'a str>,
49 pub(crate) password: Option<&'a str>,
50 pub(crate) role: Option<RoleUI>,
51 #[field(name = uncased("managerId"))]
52 pub(crate) manager_id: Option<i32>,
53}
54
55#[derive(Debug, FromForm)]
57pub(crate) struct BatchUpdateForm {
58 pub(crate) ids: Vec<i32>,
59 pub(crate) enabled: Option<bool>,
60 pub(crate) role: Option<RoleUI>,
61 #[field(name = uncased("managerId"))]
62 pub(crate) manager_id: Option<i32>,
63}
64
65#[doc(hidden)]
66pub fn routes() -> Vec<Route> {
67 routes![post, get_one, get_ids, update_one, update_many]
68}
69
70#[post("/", data = "<form>")]
80async fn post(
81 form: Form<CreateForm<'_>>,
82 db: &State<DB>,
83 user: User,
84) -> Result<WithResource<User>, MyError> {
85 debug!("----- post ----- {}", user);
86 user.can_manage_users()?;
87
88 let z_role = Role::from(form.role);
92 if !user.is_root() {
95 if !matches!(z_role, Role::User | Role::AuthUser) {
97 return Err(MyError::HTTP {
98 status: Status::Forbidden,
99 info: format!("Admin ({user}) can only create users w/ [Auth]User roles").into(),
100 });
101 }
102 }
103 let x = insert_user(db.pool(), (form.email, form.password, z_role, user.id)).await?;
104 emit_user_response(x, false).await
105}
106
107#[get("/<id>")]
113async fn get_one(id: i32, db: &State<DB>, user: User) -> Result<WithResource<User>, MyError> {
114 debug!("----- get_one ----- {}", user);
115 user.can_manage_users()?;
116
117 if user.is_root() {
118 let x = find_user(db.pool(), id)
119 .map_err(|x| x.with_status(Status::NotFound))
120 .await?;
121 match x {
122 Some(y) => emit_response!(Headers::default(), y => User),
123 None => Err(MyError::HTTP {
124 status: Status::NotFound,
125 info: format!("User #{id} not found").into(),
126 }),
127 }
128 } else if user.is_admin() {
129 let x = find_group_user(db.pool(), id, user.id)
130 .map_err(|x| x.with_status(Status::NotFound))
131 .await?;
132 match x {
133 Some(y) => emit_response!(Headers::default(), y => User),
134 None => Err(MyError::HTTP {
135 status: Status::NotFound,
136 info: format!("User #{id} not found").into(),
137 }),
138 }
139 } else {
140 Err(MyError::HTTP {
141 status: Status::Forbidden,
142 info: "Only Root and Admins can fetch users".into(),
143 })
144 }
145}
146
147#[get("/")]
150async fn get_ids(db: &State<DB>, user: User) -> Result<Json<Vec<i32>>, MyError> {
151 debug!("----- get_ids ----- {}", user);
152 user.can_manage_users()?;
153
154 if user.is_root() {
155 let x = find_all_ids(db.pool())
156 .map_err(|x| x.with_status(Status::NotFound))
157 .await?;
158 Ok(Json(x))
159 } else if user.is_admin() {
160 let x = find_group_member_ids(db.pool(), user.id)
161 .map_err(|x| x.with_status(Status::NotFound))
162 .await?;
163 Ok(Json(x))
164 } else {
165 Err(MyError::HTTP {
166 status: Status::Forbidden,
167 info: "Only Root and Admins can fetch users IDs".into(),
168 })
169 }
170}
171
172#[put("/<id>", data = "<form>")]
181async fn update_one(
182 c: Headers,
183 id: i32,
184 form: Form<UpdateForm<'_>>,
185 db: &State<DB>,
186 user: User,
187) -> Result<WithResource<User>, MyError> {
188 debug!("----- update_one ----- {user:?}");
189 debug!("form = {form:?}");
190
191 let x = find_user(db.pool(), id)
192 .map_err(|x| x.with_status(Status::NotFound))
193 .await?;
194 let old_user = match x {
195 Some(y) => y,
196 None => {
197 return Err(MyError::HTTP {
198 status: Status::NotFound,
199 info: format!("User #{id} not found").into(),
200 });
201 }
202 };
203 debug!("old_user = {old_user:?}");
204 if old_user.is_root() {
205 return Err(MyError::HTTP {
206 status: Status::BadRequest,
207 info: "Root properties are immutable".into(),
208 });
209 }
210
211 if form.enabled.is_some_and(|x| x != old_user.enabled) {
215 if !(user.is_root() || user.id == old_user.manager_id) {
217 return Err(MyError::HTTP {
218 status: Status::BadRequest,
219 info: "Only Root and the user's Admin can alter enabled flag".into(),
220 });
221 }
222 debug!("Will update enabled flag...")
223 } else if form
224 .role
225 .as_ref()
226 .is_some_and(|x| Role::from(x.0) != old_user.role)
227 {
228 if !user.is_root() {
232 if user.id != old_user.manager_id {
233 return Err(MyError::HTTP {
234 status: Status::Forbidden,
235 info: "Only Root and the user's Admin can alter roles".into(),
236 });
237 }
238
239 let new_role = Role::from(form.role.as_ref().unwrap().0);
240 if !matches!(new_role, Role::User | Role::AuthUser) {
241 return Err(MyError::HTTP {
242 status: Status::BadRequest,
243 info: "Admins can alter roles from User to AuthUser or vice-versa only".into(),
244 });
245 }
246 }
247 debug!("Will update role...")
248 } else if form.manager_id.is_some_and(|x| x != old_user.manager_id) {
249 if !user.is_root() {
251 return Err(MyError::HTTP {
252 status: Status::BadRequest,
253 info: "Only Root can alter manager_id".into(),
254 });
255 }
256 debug!("Will update manager_id...")
257 } else if form.email.is_some() || form.password.is_some() {
258 if form.email.is_none() || form.password.is_none() {
260 return Err(MyError::HTTP {
261 status: Status::BadRequest,
262 info: "When updating either 'email' or 'password' both values must be provided"
263 .into(),
264 });
265 }
266 if user.is_root() || user.id != id {
268 return Err(MyError::HTTP {
269 status: Status::BadRequest,
270 info: "Only non-Root user can alter their 'email' + 'password' fields".into(),
271 });
272 }
273 debug!("Will update email + credentials...")
274 } else {
275 return Err(MyError::HTTP {
276 status: Status::BadRequest,
277 info: "You're wasting my time :(".into(),
278 });
279 }
280
281 if c.has_no_conditionals() {
283 Err(MyError::HTTP {
284 status: Status::Conflict,
285 info: "Update User w/ no pre-conditions is NOT allowed".into(),
286 })
287 } else {
288 let x = serde_json::to_string(&old_user).map_err(|x| MyError::Data(DataError::JSON(x)))?;
289 let etag = etag_from_str(&x);
290 debug!("etag (old) = {}", etag);
291 match eval_preconditions!(&etag, c) {
292 s if s != Status::Ok => Err(MyError::HTTP {
293 status: s,
294 info: "Failed pre-condition(s)".into(),
295 }),
296 _ => {
297 let x = update_user(db.pool(), id, form.into_inner()).await?;
298 emit_user_response(x, true).await
299 }
300 }
301 }
302}
303
304#[put("/", data = "<form>")]
315async fn update_many(
316 form: Form<BatchUpdateForm>,
317 db: &State<DB>,
318 user: User,
319) -> Result<Status, MyError> {
320 debug!("----- update_many ----- {user:?}");
321 user.can_manage_users()?;
322
323 debug!("form = {form:?}");
324
325 let ids = &form.ids;
327 if ids.is_empty() {
328 info!("Empty user IDs array. Do nothing");
329 return Ok(Status::Ok);
330 }
331
332 let conn = db.pool();
333 if user.is_admin() {
335 let x = find_group_member_ids(conn, user.id).await?;
336 let ok = ids.iter().all(|id| x.contains(id));
337 if !ok {
338 return Err(MyError::HTTP {
339 status: Status::BadRequest,
340 info: "Admins can only do batch updates for users they manage".into(),
341 });
342 }
343
344 if form
346 .role
347 .as_ref()
348 .is_some_and(|x| !matches!(Role::from(x.0), Role::User | Role::AuthUser))
349 {
350 return Err(MyError::HTTP {
351 status: Status::BadRequest,
352 info: "Admins can only toggle role between User and AuthUser".into(),
353 });
354 }
355
356 if form.manager_id.is_some() {
358 return Err(MyError::HTTP {
359 status: Status::Forbidden,
360 info: "Only Root can re-assign manager ID".into(),
361 });
362 }
363 }
364
365 batch_update_users(db.pool(), form.into_inner()).await?;
366 User::clear_cache().await;
369
370 Ok(Status::Ok)
371}
372
373async fn emit_user_response(u: User, uncache: bool) -> Result<WithResource<User>, MyError> {
376 let x = serde_json::to_string(&u).map_err(|x| MyError::Data(DataError::JSON(x)))?;
377 let etag = etag_from_str(&x);
378 debug!("etag (new) = {}", etag);
379 let last_modified = get_consistent_thru()
380 .await
381 .to_rfc3339_opts(SecondsFormat::Millis, true);
382
383 if uncache {
384 u.uncache().await
385 }
386
387 Ok(WithResource {
388 inner: rocket::serde::json::Json(u),
389 etag: Header::new(header::ETAG.as_str(), etag.to_string()),
390 last_modified: Header::new(header::LAST_MODIFIED.as_str(), last_modified),
391 })
392}