mirror of
https://github.com/kanidm/kanidm.git
synced 2025-04-13 13:55:39 +02:00
Fixup email submission, tested
This commit is contained in:
parent
bb0e759134
commit
339a20947a
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -5144,7 +5144,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "9d2de91cf02bbc07cde38891769ccd5d4f073d22a40683aa4bc7a95781aaa2c4"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"indexmap 2.7.1",
|
||||
"indexmap 2.8.0",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
|
|
|
@ -132,7 +132,7 @@ pub fn view_router() -> Router<ServerState> {
|
|||
.route("/api/cu_commit", post(reset::commit))
|
||||
.route(
|
||||
"/api/user_settings/add_email",
|
||||
post(profile::view_new_email_entry_partial),
|
||||
get(profile::view_new_email_entry_partial),
|
||||
)
|
||||
.route(
|
||||
"/api/user_settings/edit_profile",
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
use kanidm_proto::attribute::Attribute;
|
||||
use super::constants::{ProfileMenuItems, Urls};
|
||||
use super::errors::HtmxError;
|
||||
use super::login::{LoginDisplayCtx, Reauth, ReauthPurpose};
|
||||
|
@ -9,7 +8,7 @@ use crate::https::middleware::KOpId;
|
|||
use crate::https::ServerState;
|
||||
use askama::Template;
|
||||
use askama_axum::IntoResponse;
|
||||
use axum::extract::State;
|
||||
use axum::extract::{Query, State};
|
||||
use axum::http::Uri;
|
||||
use axum::response::Response;
|
||||
use axum::Extension;
|
||||
|
@ -17,9 +16,11 @@ use axum_extra::extract::cookie::CookieJar;
|
|||
use axum_extra::extract::Form;
|
||||
use axum_htmx::{HxEvent, HxPushUrl, HxResponseTrigger};
|
||||
use futures_util::TryFutureExt;
|
||||
use kanidm_proto::attribute::Attribute;
|
||||
use kanidm_proto::constants::{ATTR_DISPLAYNAME, ATTR_MAIL};
|
||||
use kanidm_proto::internal::UserAuthToken;
|
||||
use kanidm_proto::scim_v1::server::{ScimEffectiveAccess, ScimPerson};
|
||||
use kanidm_proto::scim_v1::ScimMail;
|
||||
use kanidmd_lib::filter::{f_id, Filter};
|
||||
use kanidmd_lib::prelude::f_and;
|
||||
use kanidmd_lib::prelude::FC;
|
||||
|
@ -46,17 +47,26 @@ struct ProfilePartialView {
|
|||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub(crate) struct ProfileAttributes {
|
||||
pub(crate) struct SaveProfileQuery {
|
||||
#[serde(rename = "name")]
|
||||
account_name: String,
|
||||
#[serde(rename = "displayname")]
|
||||
display_name: String,
|
||||
#[serde(rename = "email_index")]
|
||||
emails_indexes: Vec<u16>,
|
||||
#[serde(rename = "emails[]")]
|
||||
emails: Vec<String>,
|
||||
// radio buttons are used to pick a primary index
|
||||
// radio buttons are used to pick a primary index, remove causes holes, map back into [emails] using [emails_indexes]
|
||||
primary_email_index: u16,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub(crate) struct ProfileAttributes {
|
||||
account_name: String,
|
||||
display_name: String,
|
||||
emails: Vec<ScimMail>,
|
||||
}
|
||||
|
||||
#[derive(Template, Clone)]
|
||||
#[template(path = "user_settings/profile_changes_partial.html")]
|
||||
struct ProfileChangesPartialView {
|
||||
|
@ -67,14 +77,12 @@ struct ProfileChangesPartialView {
|
|||
}
|
||||
|
||||
#[derive(Template, Clone)]
|
||||
#[template(path = "user_settings/form_modifiable_entry_modifiable_list_partial.html")]
|
||||
// Modifiable entry in a modifiable list partial
|
||||
pub(crate) struct FormModEntryModListPartial {
|
||||
can_rw: bool,
|
||||
r#type: String,
|
||||
name: String,
|
||||
#[template(path = "user_settings/form_email_entry_partial.html")]
|
||||
pub(crate) struct FormEmailEntryListPartial {
|
||||
can_edit: bool,
|
||||
value: String,
|
||||
invalid_feedback: String,
|
||||
primary: bool,
|
||||
index: u16,
|
||||
}
|
||||
|
||||
impl Display for ProfileAttributes {
|
||||
|
@ -94,12 +102,14 @@ pub(crate) async fn view_profile_get(
|
|||
.handle_whoami_uat(client_auth_info.clone(), kopid.eventid)
|
||||
.await?;
|
||||
|
||||
let (scim_person, scim_effective_access) = crate::https::views::admin::persons::get_person_info(
|
||||
uat.uuid,
|
||||
state,
|
||||
&kopid,
|
||||
client_auth_info.clone()).await?;
|
||||
|
||||
let (scim_person, scim_effective_access) =
|
||||
crate::https::views::admin::persons::get_person_info(
|
||||
uat.uuid,
|
||||
state,
|
||||
&kopid,
|
||||
client_auth_info.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let time = time::OffsetDateTime::now_utc() + time::Duration::new(60, 0);
|
||||
|
||||
|
@ -112,7 +122,7 @@ pub(crate) async fn view_profile_get(
|
|||
menu_active_item: ProfileMenuItems::UserProfile,
|
||||
can_rw,
|
||||
person: scim_person,
|
||||
scim_effective_access
|
||||
scim_effective_access,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
@ -123,7 +133,7 @@ pub(crate) async fn view_profile_diff_start_save_post(
|
|||
VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
|
||||
DomainInfo(domain_info): DomainInfo,
|
||||
// Form must be the last parameter because it consumes the request body
|
||||
Form(new_attrs): Form<ProfileAttributes>,
|
||||
Form(query): Form<SaveProfileQuery>,
|
||||
) -> axum::response::Result<Response> {
|
||||
let uat: UserAuthToken = state
|
||||
.qe_r_ref
|
||||
|
@ -138,13 +148,34 @@ pub(crate) async fn view_profile_diff_start_save_post(
|
|||
uat.uuid,
|
||||
state,
|
||||
&kopid,
|
||||
client_auth_info.clone()).await?;
|
||||
client_auth_info.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let primary_index = query
|
||||
.emails_indexes
|
||||
.iter()
|
||||
.position(|ei| ei == &query.primary_email_index)
|
||||
.unwrap_or(0);
|
||||
let new_mails = query
|
||||
.emails
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(ei, email)| ScimMail {
|
||||
primary: ei == primary_index,
|
||||
value: email.to_string(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
let profile_view = ProfileChangesPartialView {
|
||||
menu_active_item: ProfileMenuItems::UserProfile,
|
||||
can_rw,
|
||||
person: scim_person,
|
||||
new_attrs
|
||||
new_attrs: ProfileAttributes {
|
||||
account_name: query.account_name,
|
||||
display_name: query.display_name,
|
||||
emails: new_mails,
|
||||
},
|
||||
};
|
||||
|
||||
Ok((
|
||||
|
@ -160,7 +191,7 @@ pub(crate) async fn view_profile_diff_confirm_save_post(
|
|||
VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
|
||||
DomainInfo(domain_info): DomainInfo,
|
||||
// Form must be the last parameter because it consumes the request body
|
||||
Form(new_attrs): Form<ProfileAttributes>,
|
||||
Form(mut new_attrs): Form<ProfileAttributes>,
|
||||
) -> axum::response::Result<Response> {
|
||||
let uat: UserAuthToken = state
|
||||
.qe_r_ref
|
||||
|
@ -184,13 +215,17 @@ pub(crate) async fn view_profile_diff_confirm_save_post(
|
|||
.map_err(|op_err| HtmxError::new(&kopid, op_err, domain_info.clone()))
|
||||
.await?;
|
||||
|
||||
new_attrs
|
||||
.emails
|
||||
.sort_by_key(|sm| if sm.primary { 0 } else { 1 });
|
||||
let email_addresses = new_attrs.emails.into_iter().map(|sm| sm.value).collect();
|
||||
state
|
||||
.qe_w_ref
|
||||
.handle_setattribute(
|
||||
client_auth_info.clone(),
|
||||
uat.uuid.to_string(),
|
||||
ATTR_MAIL.to_string(),
|
||||
new_attrs.emails,
|
||||
email_addresses,
|
||||
filter.clone(),
|
||||
kopid.eventid,
|
||||
)
|
||||
|
@ -229,29 +264,37 @@ pub(crate) async fn view_profile_diff_confirm_save_post(
|
|||
State(state),
|
||||
Extension(kopid),
|
||||
VerifiedClientInformation(client_auth_info),
|
||||
DomainInfo(domain_info)
|
||||
).await {
|
||||
DomainInfo(domain_info),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(pv) => Ok(pv.into_response()),
|
||||
Err(e) => Ok(e.into_response()),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub(crate) struct AddEmailQuery {
|
||||
// the last email index is passed so we can return an incremented id
|
||||
email_index: Option<u16>,
|
||||
}
|
||||
|
||||
// Sends the user a new email input to fill in :)
|
||||
pub(crate) async fn view_new_email_entry_partial(
|
||||
State(_state): State<ServerState>,
|
||||
VerifiedClientInformation(_client_auth_info): VerifiedClientInformation,
|
||||
Extension(_kopid): Extension<KOpId>,
|
||||
Query(email_query): Query<AddEmailQuery>,
|
||||
) -> axum::response::Result<Response> {
|
||||
let add_email_trigger =
|
||||
HxResponseTrigger::after_swap([HxEvent::new("addEmailSwapped".to_string())]);
|
||||
Ok((
|
||||
add_email_trigger,
|
||||
FormModEntryModListPartial {
|
||||
can_rw: true,
|
||||
r#type: "email".to_string(),
|
||||
name: "emails[]".to_string(),
|
||||
FormEmailEntryListPartial {
|
||||
can_edit: true,
|
||||
value: "".to_string(),
|
||||
invalid_feedback: "Please enter a valid email address.".to_string(),
|
||||
primary: email_query.email_index.is_none(),
|
||||
index: email_query.email_index.map(|i| i + 1).unwrap_or(0),
|
||||
},
|
||||
)
|
||||
.into_response())
|
||||
|
|
4
server/core/static/external/forms.js
vendored
4
server/core/static/external/forms.js
vendored
|
@ -6,9 +6,9 @@ function rehook_string_list_removers() {
|
|||
if (button.getAttribute("kanidm_hooked") !== null) continue
|
||||
|
||||
button.addEventListener("click", (e) => {
|
||||
// Expected html nesting: li > div.input-group > button.kanidm-remove-list-entry
|
||||
// Expected html nesting: div.email-entry > div.input-group > button.kanidm-remove-list-entry
|
||||
let li = button.parentElement?.parentElement;
|
||||
if (li && li.tagName === "LI") {
|
||||
if (li && li.classList.contains("email-entry")) {
|
||||
li.remove();
|
||||
}
|
||||
})
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
<div class="email-entry">
|
||||
<input hidden class="email-index-state" type="text" name="email_index" value="((index))">
|
||||
<div class="input-group mb-1">
|
||||
|
||||
<div class="input-group-text">
|
||||
<input class="form-check-input mt-0" name="primary_email_index" type="radio" value="((index))" aria-label="Primary email radio button for following text input" (% if primary %) checked (% endif %)(% if !can_edit %) disabled (% endif %)>
|
||||
</div>
|
||||
|
||||
(% if can_edit %)
|
||||
<input type="email" aria-label="Email address input ((index))" class="form-control" name="emails[]" value="(( value ))" hx-validate="true" required>
|
||||
<button type="button" class="btn btn-secondary kanidm-remove-list-entry">Remove</button>
|
||||
(% else %)
|
||||
<input type="email" aria-label="Email address input ((index))" class="form-control" name="emails[]" value="(( value ))" hx-validate="true" required disabled>
|
||||
(% endif %)
|
||||
</div>
|
||||
<div class="invalid-feedback">Please enter a valid email address.</div>
|
||||
</div>
|
|
@ -13,7 +13,7 @@ Profile Difference
|
|||
<input type="hidden" name="display_name" value="(( new_attrs.display_name ))"/>
|
||||
<!-- <input type="hidden" name="legal_name" value=" new_attrs.legal_name "/>-->
|
||||
(% for email in new_attrs.emails %)
|
||||
<input type="hidden" name="emails[]" value="(( email ))"/>
|
||||
<input type="hidden" name="emails[]" value="(( email.value ))"/>
|
||||
(% endfor %)
|
||||
|
||||
<table class="table table-bordered table-responsive">
|
||||
|
@ -54,7 +54,7 @@ Profile Difference
|
|||
<td class="text-break">
|
||||
<ul>
|
||||
(% for email in new_attrs.emails %)
|
||||
<li>(( email ))</li>
|
||||
<li>(( email.value ))</li>
|
||||
(% endfor %)
|
||||
</ul>
|
||||
</td>
|
||||
|
|
|
@ -21,17 +21,26 @@ Profile
|
|||
|
||||
(% block settings_window %)
|
||||
|
||||
<form id="user_settings_container" class="needs-validation" hx-post="/ui/api/user_settings/edit_profile"
|
||||
hx-target="#user_settings_container" hx-swap="outerHTML" hx-validate="true" hx-ext="bs-validation" novalidate>
|
||||
<form id="user_settings_container" class="needs-validation"
|
||||
hx-post="/ui/api/user_settings/edit_profile"
|
||||
hx-target="#user_settings_container"
|
||||
hx-swap="outerHTML"
|
||||
hx-validate="true"
|
||||
hx-ext="bs-validation"
|
||||
novalidate>
|
||||
(% call string_attr("Name", "name", person.name, true, Attribute::Name) %)
|
||||
|
||||
(% call string_attr("Displayname", "displayname", person.displayname, true, Attribute::DisplayName) %)
|
||||
|
||||
<div class="mb-2">
|
||||
<div class="mt-3 mb-2 col-12 col-md-11 col-lg-8">
|
||||
<label for="profileEmail" class="fw-bold">Email addresses (select primary)</label>
|
||||
<label for="profileEmail" class="fw-bold">Email addresses (% if can_rw %)(selected => primary)(% else %)(select primary)(% endif %)</label>
|
||||
(% if can_rw %)
|
||||
<a class="cursor-pointer float-end" hx-boost="true" hx-post="/ui/api/user_settings/add_email" hx-target="#emailAddresses"
|
||||
<a class="cursor-pointer float-end"
|
||||
hx-boost="true"
|
||||
hx-get="/ui/api/user_settings/add_email"
|
||||
hx-target="#emailAddresses"
|
||||
hx-include="#emailAddresses :last-child .email-index-state"
|
||||
hx-swap="beforeend">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="bi bi-plus-square"
|
||||
viewBox="0 0 16 16" width="20" height="20">
|
||||
|
@ -39,29 +48,17 @@ Profile
|
|||
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4"></path>
|
||||
</svg>
|
||||
</a>
|
||||
(% endif %)
|
||||
</div>
|
||||
(% endif %)
|
||||
<div>
|
||||
<div class="row g-0">
|
||||
<div class="col-12 col-md-11 col-lg-8" id="emailAddresses">
|
||||
(% for (i, email) in person.mails.iter().enumerate() %)
|
||||
(% let type = "email" %)
|
||||
(% let name = "emails[]" %)
|
||||
(% for (index, email) in person.mails.iter().enumerate() %)
|
||||
(% let value = email.value.clone() %)
|
||||
(% let invalid_feedback = "Please enter a valid email address." %)
|
||||
(% let primary = email.primary %)
|
||||
(% let can_edit = scim_effective_access.modify_present.check(Attribute::Mail|as_ref) %)
|
||||
|
||||
<div class="input-group mb-1">
|
||||
<div class="input-group-text">
|
||||
<input class="form-check-input mt-0" name="primary_email_index" type="radio" value="((i))" aria-label="Primary email radio button for following text input">
|
||||
</div>
|
||||
|
||||
<input type="(( type ))" class="form-control" name="(( name ))" value="(( value ))" hx-validate="true" required>
|
||||
(% if can_rw %)
|
||||
<button type="button" class="btn btn-secondary kanidm-remove-list-entry">Remove</button>
|
||||
(% endif %)
|
||||
|
||||
</div>
|
||||
<div class="invalid-feedback">(( invalid_feedback ))</div>
|
||||
(% include "user_settings/form_email_entry_partial.html" %)
|
||||
|
||||
(% endfor %)
|
||||
</div>
|
||||
|
|
Loading…
Reference in a new issue