#![doc = include_str!("../../../doc/EXT_USERS.md")]
use crate::{
DataError, MyError,
db::user::{
batch_update_users, find_all_ids, find_group_member_ids, find_group_user, find_user,
insert_user, update_user,
},
emit_response, eval_preconditions,
lrs::{
DB, Headers, Role, User, etag_from_str, resources::WithResource,
server::get_consistent_thru,
},
};
use chrono::SecondsFormat;
use rocket::{
FromForm, Route, State,
form::Form,
futures::TryFutureExt,
get,
http::{Header, Status, hyper::header},
post, put, routes,
serde::json::Json,
};
use tracing::{debug, info};
#[derive(Debug, FromForm)]
struct CreateForm<'a> {
email: &'a str,
password: &'a str,
#[field(validate = range(0..4))]
role: u16,
}
#[derive(Debug, FromForm)]
#[field(validate = range(0..4))]
pub(crate) struct RoleUI(pub(crate) u16);
#[derive(Debug, FromForm)]
pub(crate) struct UpdateForm<'a> {
pub(crate) enabled: Option<bool>,
pub(crate) email: Option<&'a str>,
pub(crate) password: Option<&'a str>,
pub(crate) role: Option<RoleUI>,
#[field(name = uncased("managerId"))]
pub(crate) manager_id: Option<i32>,
}
#[derive(Debug, FromForm)]
pub(crate) struct BatchUpdateForm {
pub(crate) ids: Vec<i32>,
pub(crate) enabled: Option<bool>,
pub(crate) role: Option<RoleUI>,
#[field(name = uncased("managerId"))]
pub(crate) manager_id: Option<i32>,
}
#[doc(hidden)]
pub fn routes() -> Vec<Route> {
routes![post, get_one, get_ids, update_one, update_many]
}
#[post("/", data = "<form>")]
async fn post(
form: Form<CreateForm<'_>>,
db: &State<DB>,
user: User,
) -> Result<WithResource<User>, MyError> {
debug!("----- post ----- {}", user);
user.can_manage_users()?;
let z_role = Role::from(form.role);
if !user.is_root() {
if !matches!(z_role, Role::User | Role::AuthUser) {
return Err(MyError::HTTP {
status: Status::Forbidden,
info: format!("Admin ({user}) can only create users w/ [Auth]User roles").into(),
});
}
}
let x = insert_user(db.pool(), (form.email, form.password, z_role, user.id)).await?;
emit_user_response(x, false).await
}
#[get("/<id>")]
async fn get_one(id: i32, db: &State<DB>, user: User) -> Result<WithResource<User>, MyError> {
debug!("----- get_one ----- {}", user);
user.can_manage_users()?;
if user.is_root() {
let x = find_user(db.pool(), id)
.map_err(|x| x.with_status(Status::NotFound))
.await?;
match x {
Some(y) => emit_response!(Headers::default(), y => User),
None => Err(MyError::HTTP {
status: Status::NotFound,
info: format!("User #{id} not found").into(),
}),
}
} else if user.is_admin() {
let x = find_group_user(db.pool(), id, user.id)
.map_err(|x| x.with_status(Status::NotFound))
.await?;
match x {
Some(y) => emit_response!(Headers::default(), y => User),
None => Err(MyError::HTTP {
status: Status::NotFound,
info: format!("User #{id} not found").into(),
}),
}
} else {
Err(MyError::HTTP {
status: Status::Forbidden,
info: "Only Root and Admins can fetch users".into(),
})
}
}
#[get("/")]
async fn get_ids(db: &State<DB>, user: User) -> Result<Json<Vec<i32>>, MyError> {
debug!("----- get_ids ----- {}", user);
user.can_manage_users()?;
if user.is_root() {
let x = find_all_ids(db.pool())
.map_err(|x| x.with_status(Status::NotFound))
.await?;
Ok(Json(x))
} else if user.is_admin() {
let x = find_group_member_ids(db.pool(), user.id)
.map_err(|x| x.with_status(Status::NotFound))
.await?;
Ok(Json(x))
} else {
Err(MyError::HTTP {
status: Status::Forbidden,
info: "Only Root and Admins can fetch users IDs".into(),
})
}
}
#[put("/<id>", data = "<form>")]
async fn update_one(
c: Headers,
id: i32,
form: Form<UpdateForm<'_>>,
db: &State<DB>,
user: User,
) -> Result<WithResource<User>, MyError> {
debug!("----- update_one ----- {user:?}");
debug!("form = {form:?}");
let x = find_user(db.pool(), id)
.map_err(|x| x.with_status(Status::NotFound))
.await?;
let old_user = match x {
Some(y) => y,
None => {
return Err(MyError::HTTP {
status: Status::NotFound,
info: format!("User #{id} not found").into(),
});
}
};
debug!("old_user = {old_user:?}");
if old_user.is_root() {
return Err(MyError::HTTP {
status: Status::BadRequest,
info: "Root properties are immutable".into(),
});
}
if form.enabled.is_some_and(|x| x != old_user.enabled) {
if !(user.is_root() || user.id == old_user.manager_id) {
return Err(MyError::HTTP {
status: Status::BadRequest,
info: "Only Root and the user's Admin can alter enabled flag".into(),
});
}
debug!("Will update enabled flag...")
} else if form
.role
.as_ref()
.is_some_and(|x| Role::from(x.0) != old_user.role)
{
if !user.is_root() {
if user.id != old_user.manager_id {
return Err(MyError::HTTP {
status: Status::Forbidden,
info: "Only Root and the user's Admin can alter roles".into(),
});
}
let new_role = Role::from(form.role.as_ref().unwrap().0);
if !matches!(new_role, Role::User | Role::AuthUser) {
return Err(MyError::HTTP {
status: Status::BadRequest,
info: "Admins can alter roles from User to AuthUser or vice-versa only".into(),
});
}
}
debug!("Will update role...")
} else if form.manager_id.is_some_and(|x| x != old_user.manager_id) {
if !user.is_root() {
return Err(MyError::HTTP {
status: Status::BadRequest,
info: "Only Root can alter manager_id".into(),
});
}
debug!("Will update manager_id...")
} else if form.email.is_some() || form.password.is_some() {
if form.email.is_none() || form.password.is_none() {
return Err(MyError::HTTP {
status: Status::BadRequest,
info: "When updating either 'email' or 'password' both values must be provided"
.into(),
});
}
if user.is_root() || user.id != id {
return Err(MyError::HTTP {
status: Status::BadRequest,
info: "Only non-Root user can alter their 'email' + 'password' fields".into(),
});
}
debug!("Will update email + credentials...")
} else {
return Err(MyError::HTTP {
status: Status::BadRequest,
info: "You're wasting my time :(".into(),
});
}
if c.has_no_conditionals() {
Err(MyError::HTTP {
status: Status::Conflict,
info: "Update User w/ no pre-conditions is NOT allowed".into(),
})
} else {
let x = serde_json::to_string(&old_user).map_err(|x| MyError::Data(DataError::JSON(x)))?;
let etag = etag_from_str(&x);
debug!("etag (old) = {}", etag);
match eval_preconditions!(&etag, c) {
s if s != Status::Ok => Err(MyError::HTTP {
status: s,
info: "Failed pre-condition(s)".into(),
}),
_ => {
let x = update_user(db.pool(), id, form.into_inner()).await?;
emit_user_response(x, true).await
}
}
}
}
#[put("/", data = "<form>")]
async fn update_many(
form: Form<BatchUpdateForm>,
db: &State<DB>,
user: User,
) -> Result<Status, MyError> {
debug!("----- update_many ----- {user:?}");
user.can_manage_users()?;
debug!("form = {form:?}");
let ids = &form.ids;
if ids.is_empty() {
info!("Empty user IDs array. Do nothing");
return Ok(Status::Ok);
}
let conn = db.pool();
if user.is_admin() {
let x = find_group_member_ids(conn, user.id).await?;
let ok = ids.iter().all(|id| x.contains(id));
if !ok {
return Err(MyError::HTTP {
status: Status::BadRequest,
info: "Admins can only do batch updates for users they manage".into(),
});
}
if form
.role
.as_ref()
.is_some_and(|x| !matches!(Role::from(x.0), Role::User | Role::AuthUser))
{
return Err(MyError::HTTP {
status: Status::BadRequest,
info: "Admins can only toggle role between User and AuthUser".into(),
});
}
if form.manager_id.is_some() {
return Err(MyError::HTTP {
status: Status::Forbidden,
info: "Only Root can re-assign manager ID".into(),
});
}
}
batch_update_users(db.pool(), form.into_inner()).await?;
User::clear_cache().await;
Ok(Status::Ok)
}
async fn emit_user_response(u: User, uncache: bool) -> Result<WithResource<User>, MyError> {
let x = serde_json::to_string(&u).map_err(|x| MyError::Data(DataError::JSON(x)))?;
let etag = etag_from_str(&x);
debug!("etag (new) = {}", etag);
let last_modified = get_consistent_thru()
.await
.to_rfc3339_opts(SecondsFormat::Millis, true);
if uncache {
u.uncache().await
}
Ok(WithResource {
inner: rocket::serde::json::Json(u),
etag: Header::new(header::ETAG.as_str(), etag.to_string()),
last_modified: Header::new(header::LAST_MODIFIED.as_str(), last_modified),
})
}