Add admin UI for creating a user

This commit is contained in:
Olivier 'reivilibre 2025-07-19 15:03:38 +01:00
parent 9c966ea7f1
commit f988388e50
6 changed files with 183 additions and 4 deletions

View File

@ -21,7 +21,7 @@ use uuid::Uuid;
use crate::{
config::Configuration,
passwords::create_password_hash,
store::{IdCoopStore, RoleInfo, User, UserInfo},
store::{CreateUser, IdCoopStore, RoleInfo, User, UserInfo},
utils::Clock,
web::make_login_redirect,
};
@ -36,6 +36,10 @@ pub(crate) fn admin_router() -> Router<()> {
Router::new()
.route("/", get(get_admin_users))
.route("/users", get(get_admin_users))
.route(
"/add_user",
get(admin_get_add_user).post(admin_post_add_user),
)
.route(
"/users/{user_id}",
get(admin_get_user).post(admin_post_user),
@ -514,3 +518,131 @@ async fn admin_post_add_roles(
)
.await
}
#[derive(formbeam_derive::Form)]
struct AddUserForm {
#[form(min_chars(3), max_chars(36), regex(r"[a-z][a-z0-9]+"))]
username: String,
locked: bool,
/// Hidden XSRF token, used to check this request isn't being made from another site
/// (i.e. protects against cross-site request forgery)
xsrf: String,
}
async fn admin_get_add_user(
ambient: Ambient,
login_session: LoginSession,
Extension(clock): Extension<Clock>,
DesiredLocale(locale): DesiredLocale,
) -> WebResult<Response> {
let form = ReflectedForm::<AddUserFormRaw>::default();
let xsrf_token = login_session
.generate_xsrf_token(clock.now_utc())
.expect("must be able to create a XSRF token");
Ok(Rendered(
render_template_string!(TEMPLATING, admin_add_user, locale, {
ambient,
form,
xsrf_token
}),
)
.into_response())
}
async fn admin_post_add_user(
ambient: Ambient,
Extension(store): Extension<Arc<IdCoopStore>>,
DesiredLocale(locale): DesiredLocale,
login_session: LoginSession,
Extension(clock): Extension<Clock>,
Form(form_raw): Form<AddUserFormRaw>,
) -> WebResult<Response> {
let mut validation = form_raw
.validate()
.await
.context("failed to run form validator")?;
if !validation.is_valid() {
let form = ReflectedForm::new(form_raw, validation);
let xsrf_token = login_session
.generate_xsrf_token(clock.now_utc())
.expect("must be able to create a XSRF token");
return Ok((
StatusCode::BAD_REQUEST,
Rendered(
render_template_string!(TEMPLATING, admin_add_user, locale, {
ambient,
form,
xsrf_token
}),
),
)
.into_response());
}
// INVARIANT: Form fields are syntactically valid at this point (but note XSRF not checked)
let form = form_raw
.form()
.map_err(|e| eyre!("failed to extract a so-called valid form: {e}"))?;
if login_session
.validate_xsrf_token(&form.xsrf, clock.now_utc())
.is_err()
{
// Invalid XSRF token: try again
validation.xsrf.push(errors::xsrf_invalid());
let form = ReflectedForm::new(form_raw, validation);
let xsrf_token = login_session
.generate_xsrf_token(clock.now_utc())
.expect("must be able to create a XSRF token");
return Ok((
StatusCode::BAD_REQUEST,
Rendered(
render_template_string!(TEMPLATING, admin_add_user, locale, {
ambient,
form,
xsrf_token
}),
),
)
.into_response());
}
// INVARIANT: Form validated at this point
// Create the user
let user_id = store
.txn(|mut txn| {
Box::pin(async move {
let user_id = txn
.create_user(CreateUser {
user_login_name: form
.username
.parse()
.expect("form validation should already know username is valid"),
password_hash: None,
locked: form.locked,
})
.await?;
Ok(user_id)
})
})
.await?;
// Redirect to the user page
Ok((
StatusCode::FOUND,
[("Location", format!("/admin/users/{user_id}"))],
"User created successfully. Taking you to the user page.",
)
.into_response())
}

View File

@ -28,7 +28,7 @@
// Forms
"forms/basics": true,
"forms/checkbox-radio-switch": false,
"forms/checkbox-radio-switch": true,
"forms/input-color": false,
"forms/input-date": false,
"forms/input-file": false,

View File

@ -11,6 +11,9 @@ declare
// and this will be set automatically.
param $type = "text"
// ID of an element that describes this field.
param $aria-describedby = None
set $minlength = None
set $maxlength = None
@ -18,6 +21,7 @@ set $required = None
set $email = None
set $pattern = None
for $validator in $form.info.field_validators($name)
match $validator
MinLength($l) =>
@ -34,7 +38,7 @@ for $validator in $form.info.field_validators($name)
"$validator"
input {$type, $name, $minlength?, $maxlength?, $required?, $pattern?}
input {$type, $name, $minlength?, $maxlength?, $required?, $pattern?, $aria-describedby?}
set $errs = $form.errors.__get($name)
if $errs.len() != 0

View File

@ -0,0 +1,38 @@
AdminPage {$ambient}
:title
@admin_add_user_title
:main
nav {aria-label = "breadcrumb"}
ul
li
a {href = "/admin/users"}
@admin_nav_users
li
@admin_add_user_title
article
form {method = "POST"}
set $errs = $form.errors.form_wide
if $errs.len() != 0
@form_errors{count = $errs.len()}
ul
for $err in $errs
li
@login_error{errcode = $err.error_code()}
fieldset
label
@admin_users_attr_name
Text {$form, name = "username", aria-describedby="username-help"}
small#username-help
@admin_add_user_username_help
label
input {type = "checkbox", name = "locked"}
@admin_users_attr_locked
input {type = "hidden", name = "xsrf", value = $xsrf_token}
button {type = "submit"}
@admin_add_user_submit

View File

@ -6,6 +6,9 @@ AdminPage {$ambient}
h1
@admin_users_title
a.outline.lowprofile {href = "/admin/add_user", role = "button"}
@admin_add_user_title
table.striped
thead
tr
@ -45,4 +48,3 @@ AdminPage {$ambient}
set $first = false
span.font-mono
"${$role}"

View File

@ -32,3 +32,6 @@ admin_user_add_roles_btn_add = Add role
admin_user_add_roles_available = Available Roles
admin_user_add_roles_finish = Finish
admin_add_user_title = Add user
admin_add_user_submit = Add user
admin_add_user_username_help = This is the login name of the user and can't be changed later. Must only contain lowercase letters and numbers, but can't start with a number. Minimum of 3 characters. Maximum of 36 characters.