use rustlavel::prelude::*;
use rustlavel::rbac::Permissions;
use crate::models::user::User;
use crate::support::{page, tokens};
const PER_PAGE: i64 = 20;
pub struct UsersController;
impl UsersController {
pub async fn index(req: Request) -> Result<Response> {
let db = req.state::<Database>().expect("the database is registered in main.rs").clone();
let store = rbac(&req)?;
let me = req.identity().and_then(|id| id.id_as::<i64>()).unwrap_or_default();
let search = req.query("q").unwrap_or_default().trim().to_string();
let role_filter = req.query("role").unwrap_or_default().to_string();
let page_number = req.query("page").and_then(|p| p.parse::<i64>().ok()).unwrap_or(1).max(1);
let mut query = User::query().order_by("name", rustlavel::db::Direction::Asc);
if !search.is_empty() {
let pattern = format!("%{search}%");
query = query.group_filter(|q| {
q.filter_like("name", pattern.clone()).or_filter("email", pattern.clone())
});
}
if !role_filter.is_empty() {
let _ = &role_filter;
}
let listed = query.paginate(&db, page_number, PER_PAGE).await?;
let mut users = listed.hydrate::<User>()?;
if !role_filter.is_empty() {
let mut kept = Vec::new();
for user in users {
if store.has_role(user.id, &role_filter).await.unwrap_or(false) {
kept.push(user);
}
}
users = kept;
}
let now = tokens::now();
let mut rows = Vec::with_capacity(users.len());
for user in &users {
let roles = store.roles_for(user.id).await.unwrap_or_default();
let mut json = user.public_json();
if let Json::Object(fields) = &mut json {
fields.insert("locked".into(), Json::from(user.is_locked(&now)));
fields.insert(
"last_login_at".into(),
Json::from(
user.last_login_at.as_deref().map(tokens::humanise).unwrap_or_else(|| "Never".into()),
),
);
fields.insert(
"roles".into(),
Json::Array(roles.iter().map(|r| Json::from(r.as_str())).collect()),
);
fields.insert("deletable".into(), Json::from(user.id != me));
fields.insert("impersonatable".into(), Json::from(user.id != me));
}
rows.push(json);
}
let all_roles = store.roles().await.unwrap_or_default();
let mut context = page::shell(&req, "users").await;
context = with_current_user(context, &req, &db).await?;
context = context
.with("q", Json::from(search.as_str()))
.with("users_empty", Json::from(rows.is_empty()))
.with("users", Json::Array(rows))
.with(
"all_roles",
Json::Array(
all_roles
.iter()
.map(|role| {
Json::object([
("name", Json::from(role.name.as_str())),
("selected", Json::from(role.name == role_filter)),
])
})
.collect(),
),
)
.with("can_create", Json::from(req.can("users.create").await?))
.with("can_update", Json::from(req.can("users.update").await?))
.with("can_delete", Json::from(req.can("users.delete").await?))
.with("can_impersonate", Json::from(req.can("users.impersonate").await?));
context = pagination(context, &req, page_number, listed.total);
req.view("admin/users/index", &context)
}
pub async fn create(req: Request) -> Result<Response> {
let db = req.state::<Database>().expect("the database is registered in main.rs").clone();
let mut context = page::shell(&req, "users").await;
context = with_current_user(context, &req, &db).await?;
context = Self::form_context(context, &req, None).await?;
req.view("admin/users/form", &context)
}
pub async fn store(mut req: Request) -> Result<Response> {
let db = req.state::<Database>().expect("the database is registered in main.rs").clone();
let store = rbac(&req)?;
let name = req.input("name").unwrap_or_default();
let email = req.input("email").unwrap_or_default().trim().to_lowercase();
let roles = req.inputs("roles[]");
let mut errors = page::check(
&[("name", &name), ("email", &email)],
&[("name", "required|max:120"), ("email", "required|email|max:190")],
);
if User::first(&db, User::by_email(&email)).await?.is_some() {
errors.add("email", "Somebody already has that address.");
}
if !errors.is_empty() {
let mut context = page::errors(page::shell(&req, "users").await, &errors);
context = with_current_user(context, &req, &db).await?;
context = Self::form_context(context, &req, None).await?;
return req.view("admin/users/form", &context.with("name", Json::from(name)).with("email", Json::from(email)));
}
let mut user = User { name: name.trim().to_string(), email, is_active: true, ..Default::default() };
user.insert(&db).await?;
for role in &roles {
store.assign_role(user.id, role).await?;
}
crate::controllers::auth::register_controller::send_activation(
&req,
&db,
&user,
"You have been invited",
)
.await?;
page::flash(&req, "success", format!("{} has been invited.", user.name));
Ok(Response::see_other("/admin/users"))
}
pub async fn edit(req: Request) -> Result<Response> {
let db = req.state::<Database>().expect("the database is registered in main.rs").clone();
let id = req.param_as::<i64>("id").unwrap_or_default();
let Some(user) = User::find(&db, id).await? else { return Ok(Response::not_found()) };
let mut context = page::shell(&req, "users").await;
context = with_current_user(context, &req, &db).await?;
context = Self::form_context(context, &req, Some(&user)).await?;
req.view("admin/users/form", &context)
}
pub async fn update(mut req: Request) -> Result<Response> {
let db = req.state::<Database>().expect("the database is registered in main.rs").clone();
let store = rbac(&req)?;
let id = req.param_as::<i64>("id").unwrap_or_default();
let Some(mut user) = User::find(&db, id).await? else { return Ok(Response::not_found()) };
let name = req.input("name").unwrap_or_default();
let email = req.input("email").unwrap_or_default().trim().to_lowercase();
let roles = req.inputs("roles[]");
let mut errors = page::check(
&[("name", &name), ("email", &email)],
&[("name", "required|max:120"), ("email", "required|email|max:190")],
);
if let Some(other) = User::first(&db, User::by_email(&email)).await?
&& other.id != user.id
{
errors.add("email", "Somebody already has that address.");
}
if !errors.is_empty() {
let mut context = page::errors(page::shell(&req, "users").await, &errors);
context = with_current_user(context, &req, &db).await?;
context = Self::form_context(context, &req, Some(&user)).await?;
return req.view("admin/users/form", &context);
}
user.name = name.trim().to_string();
user.email = email;
user.update(&db).await?;
let held = store.roles_for(user.id).await?;
for role in held.iter().filter(|r| !roles.contains(r)) {
store.remove_role(user.id, role).await?;
}
for role in roles.iter().filter(|r| !held.contains(r)) {
store.assign_role(user.id, role).await?;
}
for permission in store.permissions().await? {
match req.input(&format!("permission[{}]", permission.name)).as_deref() {
Some("grant") => store.grant(user.id, &permission.name).await?,
Some("deny") => store.deny(user.id, &permission.name).await?,
Some("inherit") => store.reset(user.id, &permission.name).await?,
_ => {}
}
}
page::flash(&req, "success", format!("{} has been updated.", user.name));
Ok(Response::see_other("/admin/users"))
}
pub async fn destroy(req: Request) -> Result<Response> {
let db = req.state::<Database>().expect("the database is registered in main.rs").clone();
let store = rbac(&req)?;
let id = req.param_as::<i64>("id").unwrap_or_default();
let me = req.identity().and_then(|id| id.id_as::<i64>()).unwrap_or_default();
if id == me {
page::flash(&req, "error", "You cannot delete your own account.");
return Ok(Response::see_other("/admin/users"));
}
let Some(user) = User::find(&db, id).await? else { return Ok(Response::not_found()) };
store.purge_user(user.id).await?;
user.delete(&db).await?;
page::flash(&req, "warning", format!("{} has been deleted.", user.name));
Ok(Response::see_other("/admin/users"))
}
async fn form_context(
context: ViewContext,
req: &Request,
user: Option<&User>,
) -> Result<ViewContext> {
let store = rbac(req)?;
let is_new = user.is_none();
let held = match user {
Some(user) => store.roles_for(user.id).await?,
None => Vec::new(),
};
let roles: Vec<Json> = store
.roles()
.await?
.iter()
.map(|role| {
Json::object([
("name", Json::from(role.name.as_str())),
(
"description",
role.description.clone().map_or(Json::Null, Json::from),
),
("assigned", Json::from(held.contains(&role.name))),
])
})
.collect();
let mut permissions = Vec::new();
if let Some(user) = user {
let direct = store.direct_permissions(user.id).await?;
let from_roles = store.permissions_for(user.id).await?;
for permission in store.permissions().await? {
let name = permission.name.clone();
let explicit = direct.iter().find(|(p, _)| *p == name).map(|(_, granted)| *granted);
let inherited = from_roles.contains(&name) && explicit.is_none();
let choices = ["inherit", "grant", "deny"].map(|value| {
Json::object([
("value", Json::from(value)),
(
"label",
Json::from(match value {
"grant" => "Allow",
"deny" => "Deny",
_ => "Inherit",
}),
),
(
"selected",
Json::from(match (value, explicit) {
("grant", Some(true)) => true,
("deny", Some(false)) => true,
("inherit", None) => true,
_ => false,
}),
),
])
});
permissions.push(Json::object([
("name", Json::from(name)),
("from_role", if inherited { Json::from("a role") } else { Json::Null }),
("choices", Json::Array(choices.to_vec())),
]));
}
}
Ok(context
.with("is_new", Json::from(is_new))
.with("title", Json::from(if is_new { "New user" } else { "Edit user" }))
.with(
"action",
Json::from(match user {
Some(user) => format!("/admin/users/{}", user.id),
None => "/admin/users".to_string(),
}),
)
.with("submit_label", Json::from(if is_new { "Send invitation" } else { "Save changes" }))
.with("name", Json::from(user.map(|u| u.name.as_str()).unwrap_or_default()))
.with("email", Json::from(user.map(|u| u.email.as_str()).unwrap_or_default()))
.with("roles", Json::Array(roles))
.with("permissions", Json::Array(permissions)))
}
}
pub fn rbac(req: &Request) -> Result<Permissions> {
req.state::<Permissions>().cloned().ok_or_else(|| {
Error::msg(
"the roles and permissions store is not registered. Add \
`.plugin(Rbac::from_config(db.clone(), app.config()))` in main.rs.",
)
})
}
pub async fn with_current_user(
context: ViewContext,
req: &Request,
db: &Database,
) -> Result<ViewContext> {
let id = req.identity().and_then(|id| id.id_as::<i64>()).unwrap_or_default();
match User::find(db, id).await? {
Some(user) => page::with_user(context, req, &user).await,
None => Ok(context),
}
}
pub fn pagination(context: ViewContext, req: &Request, page: i64, total: i64) -> ViewContext {
let last = ((total + PER_PAGE - 1) / PER_PAGE).max(1);
let path = req.path();
let link = |n: i64| Json::from(format!("{path}?page={n}"));
context
.with("has_pages", Json::from(last > 1))
.with("page_from", Json::from(((page - 1) * PER_PAGE + 1).min(total.max(1))))
.with("page_to", Json::from((page * PER_PAGE).min(total)))
.with("page_total", Json::from(total))
.with("prev_url", if page > 1 { link(page - 1) } else { Json::Null })
.with("next_url", if page < last { link(page + 1) } else { Json::Null })
}