use askama::Template;
use axum::extract::{Path, State};
use axum::response::{IntoResponse, Redirect, Response};
use axum::{Extension, Form};
use laterite_core::query::{bind_values, build as to_sql, text_cast};
use laterite_core::AnyRowExt;
use sea_query::{Alias, Expr, Query};
use crate::{not_found, render, render_error, AdminState, Permission, Shell};
pub(crate) async fn new_form(
State(state): State<AdminState>,
Extension(shell): Extension<Shell>,
) -> Response {
render(build(&state, "/admin/roles/new", None, "", "", &[], shell))
}
pub(crate) async fn create(
State(state): State<AdminState>,
Extension(shell): Extension<Shell>,
Form(pairs): Form<Vec<(String, String)>>,
) -> Response {
let (code, name, perms) = parse(&pairs);
let perms = registered_only(perms, &state.permissions);
if code.is_empty() || name.is_empty() {
return render(build(
&state,
"/admin/roles/new",
Some("Code and name are required."),
&code,
&name,
&perms,
shell,
));
}
match laterite_auth::store::create_role(&state.db, &code, &name, &perms).await {
Ok(_) => Redirect::to("/admin/roles").into_response(),
Err(_) => render(build(
&state,
"/admin/roles/new",
Some("Could not save. The code may already be in use."),
&code,
&name,
&perms,
shell,
)),
}
}
pub(crate) async fn edit_form(
State(state): State<AdminState>,
Extension(shell): Extension<Shell>,
Path(id): Path<String>,
) -> Response {
let action = format!("/admin/roles/{id}/edit");
let (sql, values) = {
let stmt = Query::select()
.columns([
Alias::new("code"),
Alias::new("name"),
Alias::new("permissions"),
])
.from(Alias::new("backend_roles"))
.and_where(
Expr::col(Alias::new("id"))
.cast_as(Alias::new(text_cast(state.db.backend)))
.eq(id.clone()),
)
.to_owned();
to_sql(state.db.backend, stmt)
};
let row = match bind_values(sqlx::query(&sql), values)
.fetch_optional(&state.db.pool)
.await
{
Ok(row) => row,
Err(_) => return render_error(),
};
let Some(row) = row else {
return not_found();
};
let code = row.get_text("code").unwrap_or_default();
let name = row.get_text("name").unwrap_or_default();
let perms_json = row.get_text("permissions").unwrap_or_default();
let perms: Vec<String> = serde_json::from_str(&perms_json).unwrap_or_default();
render(build(&state, &action, None, &code, &name, &perms, shell))
}
pub(crate) async fn update(
State(state): State<AdminState>,
Extension(shell): Extension<Shell>,
Path(id): Path<String>,
Form(pairs): Form<Vec<(String, String)>>,
) -> Response {
let (code, name, perms) = parse(&pairs);
let perms = registered_only(perms, &state.permissions);
let action = format!("/admin/roles/{id}/edit");
if code.is_empty() || name.is_empty() {
return render(build(
&state,
&action,
Some("Code and name are required."),
&code,
&name,
&perms,
shell,
));
}
let perms_json = serde_json::to_string(&perms).unwrap_or_else(|_| "[]".to_string());
let (sql, values) = {
let stmt = Query::update()
.table(Alias::new("backend_roles"))
.value(Alias::new("code"), code.clone())
.value(Alias::new("name"), name.clone())
.value(Alias::new("permissions"), perms_json)
.and_where(
Expr::col(Alias::new("id"))
.cast_as(Alias::new(text_cast(state.db.backend)))
.eq(id.clone()),
)
.to_owned();
to_sql(state.db.backend, stmt)
};
match bind_values(sqlx::query(&sql), values)
.execute(&state.db.pool)
.await
{
Ok(_) => Redirect::to("/admin/roles").into_response(),
Err(_) => render(build(
&state,
&action,
Some("Could not save. The code may already be in use."),
&code,
&name,
&perms,
shell,
)),
}
}
fn parse(pairs: &[(String, String)]) -> (String, String, Vec<String>) {
let mut code = String::new();
let mut name = String::new();
let mut perms = Vec::new();
for (key, value) in pairs {
match key.as_str() {
"code" => code = value.trim().to_string(),
"name" => name = value.trim().to_string(),
"perm" => perms.push(value.clone()),
_ => {}
}
}
(code, name, perms)
}
fn registered_only(perms: Vec<String>, registry: &[Permission]) -> Vec<String> {
perms
.into_iter()
.filter(|p| registry.iter().any(|r| &r.code == p))
.collect()
}
fn group_permissions(registry: &[Permission], selected: &[String]) -> Vec<PermGroupView> {
let mut groups: Vec<PermGroupView> = Vec::new();
for permission in registry {
let check = PermCheckView {
code: permission.code.clone(),
label: permission.label.clone(),
checked: selected.iter().any(|s| s == &permission.code),
};
match groups.iter_mut().find(|g| g.name == permission.group) {
Some(group) => group.perms.push(check),
None => groups.push(PermGroupView {
name: permission.group.clone(),
perms: vec![check],
}),
}
}
groups
}
#[allow(clippy::too_many_arguments)]
fn build(
state: &AdminState,
action: &str,
error: Option<&str>,
code: &str,
name: &str,
selected: &[String],
shell: Shell,
) -> RolesFormTemplate {
RolesFormTemplate {
shell,
title: "Role".to_string(),
action: action.to_string(),
cancel_path: "/admin/roles".to_string(),
error: error.map(str::to_string),
code: code.to_string(),
name: name.to_string(),
groups: group_permissions(&state.permissions, selected),
}
}
struct PermCheckView {
code: String,
label: String,
checked: bool,
}
struct PermGroupView {
name: String,
perms: Vec<PermCheckView>,
}
#[derive(Template)]
#[template(path = "roles_form.html")]
struct RolesFormTemplate {
shell: Shell,
title: String,
action: String,
cancel_path: String,
error: Option<String>,
code: String,
name: String,
groups: Vec<PermGroupView>,
}
#[cfg(test)]
mod tests {
use super::*;
fn registry() -> Vec<Permission> {
vec![
Permission {
code: "backend.manage_users".to_string(),
label: "Manage backend users".to_string(),
group: "Backend".to_string(),
},
Permission {
code: "acme.publish".to_string(),
label: "Publish".to_string(),
group: "Content".to_string(),
},
]
}
#[test]
fn parse_pulls_scalars_and_repeated_perms() {
let pairs = vec![
("code".to_string(), " editor ".to_string()),
("name".to_string(), "Editor".to_string()),
("perm".to_string(), "backend.manage_users".to_string()),
("perm".to_string(), "acme.publish".to_string()),
];
let (code, name, perms) = parse(&pairs);
assert_eq!(code, "editor");
assert_eq!(name, "Editor");
assert_eq!(perms, ["backend.manage_users", "acme.publish"]);
}
#[test]
fn only_registered_permissions_survive() {
let kept = registered_only(
vec![
"backend.manage_users".to_string(),
"nope.invalid".to_string(),
],
®istry(),
);
assert_eq!(kept, ["backend.manage_users"]);
}
#[test]
fn grouping_preserves_order_and_marks_selected() {
let groups = group_permissions(®istry(), &["acme.publish".to_string()]);
assert_eq!(groups[0].name, "Backend");
assert_eq!(groups[1].name, "Content");
assert!(!groups[0].perms[0].checked);
assert!(groups[1].perms[0].checked);
}
}