admin/users: add new user

This commit is contained in:
Philippe Loctaux 2023-05-06 00:26:38 +02:00
parent 85facf7dc6
commit 306f2a60c4
8 changed files with 167 additions and 5 deletions

View file

@ -30,6 +30,7 @@ pub enum Page {
UserSecurityTotp(UserSecurityTotp), UserSecurityTotp(UserSecurityTotp),
AuthorizeTotp(AuthorizeTotp), AuthorizeTotp(AuthorizeTotp),
AdminUsersView(AdminUsersView), AdminUsersView(AdminUsersView),
AdminUsersNew(AdminUsersNew),
} }
impl Page { impl Page {
@ -56,6 +57,7 @@ impl Page {
Page::UserSecurityTotp(_) => "pages/settings/totp", Page::UserSecurityTotp(_) => "pages/settings/totp",
Page::AuthorizeTotp(_) => "pages/oauth/totp", Page::AuthorizeTotp(_) => "pages/oauth/totp",
Page::AdminUsersView(_) => "pages/admin/users/view", Page::AdminUsersView(_) => "pages/admin/users/view",
Page::AdminUsersNew(_) => "pages/admin/users/new",
} }
} }
@ -82,6 +84,7 @@ impl Page {
Page::UserSecurityTotp(_) => "Enable One-time password", Page::UserSecurityTotp(_) => "Enable One-time password",
Page::AuthorizeTotp(_) => "Verifying your account", Page::AuthorizeTotp(_) => "Verifying your account",
Page::AdminUsersView(_) => "User info", Page::AdminUsersView(_) => "User info",
Page::AdminUsersNew(_) => "New user",
} }
} }
@ -110,6 +113,7 @@ impl Page {
Page::UserSecurityTotp(_) => Some(UserMenu::Settings.into()), Page::UserSecurityTotp(_) => Some(UserMenu::Settings.into()),
Page::AuthorizeTotp(_) => None, Page::AuthorizeTotp(_) => None,
Page::AdminUsersView(_) => Some(AdminMenu::Users.into()), Page::AdminUsersView(_) => Some(AdminMenu::Users.into()),
Page::AdminUsersNew(_) => Some(AdminMenu::Users.into()),
} }
} }
@ -136,6 +140,7 @@ impl Page {
Page::UserSecurityTotp(totp) => Box::new(totp), Page::UserSecurityTotp(totp) => Box::new(totp),
Page::AuthorizeTotp(totp) => Box::new(totp), Page::AuthorizeTotp(totp) => Box::new(totp),
Page::AdminUsersView(view) => Box::new(view), Page::AdminUsersView(view) => Box::new(view),
Page::AdminUsersNew(new) => Box::new(new),
} }
} }
} }

View file

@ -24,6 +24,8 @@ pub fn routes() -> Vec<Route> {
admin_apps_new_secret, admin_apps_new_secret,
admin_apps_archive, admin_apps_archive,
admin_users_list, admin_users_list,
admin_users_new,
admin_users_new_form,
admin_users_view, admin_users_view,
admin_users_archive, admin_users_archive,
admin_users_password_reset, admin_users_password_reset,
@ -103,4 +105,11 @@ pub mod content {
pub local: User, pub local: User,
pub password_recover_expiration: Option<String>, pub password_recover_expiration: Option<String>,
} }
#[derive(Serialize)]
#[serde(crate = "rocket::serde")]
#[derive(Clone)]
pub struct AdminUsersNew {
pub user: JwtClaims,
}
} }

View file

@ -30,6 +30,62 @@ pub async fn admin_users_list(
.unwrap_or_else(|| page.into())) .unwrap_or_else(|| page.into()))
} }
#[get("/admin/users/new")]
pub async fn admin_users_new(admin: JwtAdmin, flash: Option<FlashMessage<'_>>) -> Result<Template> {
let page = Page::AdminUsersNew(super::content::AdminUsersNew { user: admin.0 });
Ok(flash
.map(|flash| Page::with_flash(page.clone(), flash))
.unwrap_or_else(|| page.into()))
}
#[derive(Debug, FromForm)]
pub struct UsersNewForm<'r> {
pub username: &'r str,
}
#[post("/admin/users/new", data = "<form>")]
pub async fn admin_users_new_form(
_admin: JwtAdmin,
mut db: Connection<Database>,
form: Form<UsersNewForm<'_>>,
) -> Result<Flash<Redirect>> {
// Parse username
let username = match Username::from_str(form.username) {
Ok(username) => username,
Err(_) => {
return Ok(Flash::new(
Redirect::to(uri!(admin_users_new)),
FlashKind::Danger,
INVALID_USERNAME_ERROR,
));
}
};
// Generate UserID
let user_id = task::spawn_blocking(UserID::default).await?;
let mut transaction = db.begin().await?;
// Insert user in database
if let Err(e) = User::insert(&mut transaction, &user_id, false, &username, None).await {
return Ok(Flash::new(
Redirect::to(uri!(admin_users_new)),
FlashKind::Danger,
e.to_string(),
));
}
transaction.commit().await?;
let id = RocketUserID(user_id);
Ok(Flash::new(
Redirect::to(uri!(admin_users_view(id))),
FlashKind::Success,
"User has been created.",
))
}
#[get("/admin/users/<id>")] #[get("/admin/users/<id>")]
pub async fn admin_users_view( pub async fn admin_users_view(
admin_not_current: JwtAdminNotCurrent, admin_not_current: JwtAdminNotCurrent,

View file

@ -0,0 +1,61 @@
{% extends "shell" %}
{% import "utils/form" as form %}
{% block content %}
<div class="page-header d-print-none">
<div class="container-xl">
<div class="row align-items-center">
<div class="col">
<div class="page-pretitle">
Admin dashboard
</div>
<h2 class="page-title">
Users
</h2>
</div>
</div>
</div>
</div>
<!-- Page body -->
<div class="page-body">
<div class="container-xl">
{% if flash %}
<div class="alert alert-{{flash.0}}" role="alert">
<h4 class="alert-title">{{ flash.1 | safe }}</h4>
</div>
{% endif %}
<form id="new_user" action="" method="post" autocomplete="off" class="card">
<div class="card-header">
<h3 class="card-title">New User</h3>
</div>
<div class="card-body">
<!-- Username -->
<div class="mb-3">
<label class="form-label required" for="username">Username</label>
<div>
<input name="username" id="username" type="text" placeholder="Enter username"
class="form-control"
required>
</div>
</div>
</div>
<div class="card-footer text-end">
<button type="submit" class="btn btn-primary">Submit</button>
</div>
</form>
</div>
</div>
{{ form::disable_button_delay_submit(form_id="new_user") }}
{% endblock content %}
{% block libs_js %}
{% endblock lib_js %}
{% block additional_js %}
{% endblock additional_js %}

View file

@ -11,7 +11,7 @@
{% if flash %} {% if flash %}
<div class="alert alert-{{flash.0}}" role="alert"> <div class="alert alert-{{flash.0}}" role="alert">
<h4 class="alert-title">{{ flash.1 }}</h4> <h4 class="alert-title">{{ flash.1 | safe }}</h4>
</div> </div>
{% endif %} {% endif %}

View file

@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize};
use std::fmt::{Display, Formatter}; use std::fmt::{Display, Formatter};
use std::str::FromStr; use std::str::FromStr;
pub const INVALID_USERNAME_ERROR: &str = "Invalid username. Pattern is [a-zA-Z0-9]"; pub const INVALID_USERNAME_ERROR: &str = "Invalid username. Pattern is <code>[a-zA-Z0-9]</code>";
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct Username(pub String); pub struct Username(pub String);

View file

@ -43,15 +43,43 @@ impl User {
is_admin: bool, is_admin: bool,
username: &Username, username: &Username,
password: Option<&Password>, password: Option<&Password>,
) -> Result<Option<()>, Error> { ) -> Result<(), Error> {
Ok(DatabaseUsers::insert( DatabaseUsers::insert(
conn, conn,
&id.0, &id.0,
is_admin, is_admin,
username.as_ref(), username.as_ref(),
password.map(|p| p.hash()), password.map(|p| p.hash()),
) )
.await?) .await
.map_err(|e| match e {
DatabaseError::UniqueConstraintPrimaryKey => Error::IdNotAvailable(id.to_string()),
DatabaseError::UniqueConstraint(column) => {
if &column == "username" {
Error::UsernameNotAvailable(username.into())
} else {
Error::ColumnNotAvailable(column)
}
}
_ => e.into(),
})?;
Ok(())
// DatabaseUsers::set_username(conn, self.id.as_ref(), username.as_ref())
// .await
// .map_err(|e| match e {
// DatabaseError::UniqueConstraint(column) => {
// if &column == "username" {
// Error::UsernameNotAvailable(username.into())
// } else {
// Error::ColumnNotAvailable(column)
// }
// }
// _ => e.into(),
// })?;
//
// Ok(())
} }
pub async fn get_by_id( pub async fn get_by_id(

View file

@ -9,6 +9,9 @@ pub enum Error {
#[error("The database column \"{0}\" is not available.")] #[error("The database column \"{0}\" is not available.")]
ColumnNotAvailable(String), ColumnNotAvailable(String),
#[error("The generated id \"{0}\" is not available. Please retry.")]
IdNotAvailable(String),
#[error("The username \"{0}\" is not available.")] #[error("The username \"{0}\" is not available.")]
UsernameNotAvailable(String), UsernameNotAvailable(String),