Compare commits

...

3 commits

Author SHA1 Message Date
Merlijn 6e41b4663e
Merge 3077d86aa1 into ad012cd6fd 2025-04-06 02:23:02 +00:00
ToxicMushroom 3077d86aa1
final fixes 2025-04-06 04:22:56 +02:00
ToxicMushroom 9b3f814b67
cargo fmt 2025-04-06 01:29:26 +02:00
3 changed files with 103 additions and 75 deletions
proto/src/scim_v1
server/core
src/https/views
templates/user_settings

View file

@ -71,7 +71,7 @@ pub enum ScimSchema {
} }
#[serde_as] #[serde_as]
#[derive(Deserialize, Serialize, Debug, Clone, ToSchema)] #[derive(Deserialize, Serialize, PartialEq, Eq, Debug, Clone, ToSchema)]
#[serde(deny_unknown_fields, rename_all = "camelCase")] #[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct ScimMail { pub struct ScimMail {
#[serde(default)] #[serde(default)]

View file

@ -17,7 +17,7 @@ use axum_extra::extract::Form;
use axum_htmx::{HxEvent, HxPushUrl, HxResponseTrigger}; use axum_htmx::{HxEvent, HxPushUrl, HxResponseTrigger};
use futures_util::TryFutureExt; use futures_util::TryFutureExt;
use kanidm_proto::attribute::Attribute; use kanidm_proto::attribute::Attribute;
use kanidm_proto::constants::{ATTR_DISPLAYNAME, ATTR_MAIL}; use kanidm_proto::constants::{ATTR_DISPLAYNAME, ATTR_MAIL, ATTR_NAME};
use kanidm_proto::internal::UserAuthToken; use kanidm_proto::internal::UserAuthToken;
use kanidm_proto::scim_v1::server::{ScimEffectiveAccess, ScimPerson}; use kanidm_proto::scim_v1::server::{ScimEffectiveAccess, ScimPerson};
use kanidm_proto::scim_v1::ScimMail; use kanidm_proto::scim_v1::ScimMail;
@ -63,13 +63,13 @@ pub(crate) struct SaveProfileQuery {
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub(crate) struct CommitSaveProfileQuery { pub(crate) struct CommitSaveProfileQuery {
#[serde(rename = "account_name")] #[serde(rename = "account_name")]
account_name: String, account_name: Option<String>,
#[serde(rename = "display_name")] #[serde(rename = "display_name")]
display_name: String, display_name: Option<String>,
#[serde(rename = "emails[]")] #[serde(rename = "emails[]")]
emails: Vec<String>, emails: Vec<String>,
#[serde(rename = "new_primary_mail")] #[serde(rename = "new_primary_mail")]
new_primary_mail: Option<String> new_primary_mail: Option<String>,
} }
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
@ -88,6 +88,7 @@ struct ProfileChangesPartialView {
primary_mail: Option<String>, primary_mail: Option<String>,
new_attrs: ProfileAttributes, new_attrs: ProfileAttributes,
new_primary_mail: Option<String>, new_primary_mail: Option<String>,
emails_are_same: bool,
} }
#[derive(Template, Clone)] #[derive(Template, Clone)]
@ -123,7 +124,7 @@ pub(crate) async fn view_profile_get(
&kopid, &kopid,
client_auth_info.clone(), client_auth_info.clone(),
) )
.await?; .await?;
let time = time::OffsetDateTime::now_utc() + time::Duration::new(60, 0); let time = time::OffsetDateTime::now_utc() + time::Duration::new(60, 0);
@ -163,7 +164,8 @@ pub(crate) async fn view_profile_diff_start_save_post(
state, state,
&kopid, &kopid,
client_auth_info.clone(), client_auth_info.clone(),
).await?; )
.await?;
let primary_index = query let primary_index = query
.emails_indexes .emails_indexes
@ -179,10 +181,13 @@ pub(crate) async fn view_profile_diff_start_save_post(
value: email.to_string(), value: email.to_string(),
}) })
.collect(); .collect();
let old_primary_mail = scim_person.mails.iter() let old_primary_mail = scim_person
.mails
.iter()
.find(|sm| sm.primary) .find(|sm| sm.primary)
.map(|sm| sm.value.clone()); .map(|sm| sm.value.clone());
let emails_are_same = scim_person.mails == new_mails;
let profile_view = ProfileChangesPartialView { let profile_view = ProfileChangesPartialView {
menu_active_item: ProfileMenuItems::UserProfile, menu_active_item: ProfileMenuItems::UserProfile,
@ -194,13 +199,15 @@ pub(crate) async fn view_profile_diff_start_save_post(
display_name: query.display_name, display_name: query.display_name,
emails: new_mails, emails: new_mails,
}, },
emails_are_same,
new_primary_mail: query.emails.get(primary_index).cloned(), new_primary_mail: query.emails.get(primary_index).cloned(),
}; };
Ok(( Ok((
HxPushUrl(Uri::from_static("/ui/profile/diff")), HxPushUrl(Uri::from_static("/ui/profile/diff")),
profile_view, profile_view,
).into_response()) )
.into_response())
} }
pub(crate) async fn view_profile_diff_confirm_save_post( pub(crate) async fn view_profile_diff_confirm_save_post(
@ -216,24 +223,37 @@ pub(crate) async fn view_profile_diff_confirm_save_post(
.handle_whoami_uat(client_auth_info.clone(), kopid.eventid) .handle_whoami_uat(client_auth_info.clone(), kopid.eventid)
.map_err(|op_err| HtmxError::new(&kopid, op_err, domain_info.clone())) .map_err(|op_err| HtmxError::new(&kopid, op_err, domain_info.clone()))
.await?; .await?;
dbg!(&query);
let filter = filter_all!(f_and!([f_id(uat.uuid.to_string().as_str())])); let filter = filter_all!(f_and!([f_id(uat.uuid.to_string().as_str())]));
state if let Some(account_name) = query.account_name {
.qe_w_ref state
.handle_setattribute( .qe_w_ref
client_auth_info.clone(), .handle_setattribute(
uat.uuid.to_string(), client_auth_info.clone(),
ATTR_DISPLAYNAME.to_string(), uat.uuid.to_string(),
vec![query.display_name], ATTR_NAME.to_string(),
filter.clone(), vec![account_name],
kopid.eventid, filter.clone(),
) kopid.eventid,
.map_err(|op_err| HtmxError::new(&kopid, op_err, domain_info.clone())) )
.await?; .map_err(|op_err| HtmxError::new(&kopid, op_err, domain_info.clone()))
.await?;
}
if let Some(display_name) = query.display_name {
state
.qe_w_ref
.handle_setattribute(
client_auth_info.clone(),
uat.uuid.to_string(),
ATTR_DISPLAYNAME.to_string(),
vec![display_name],
filter.clone(),
kopid.eventid,
)
.map_err(|op_err| HtmxError::new(&kopid, op_err, domain_info.clone()))
.await?;
}
let mut emails = query.emails; let mut emails = query.emails;
if let Some(primary) = query.new_primary_mail { if let Some(primary) = query.new_primary_mail {
emails.insert(0, primary); emails.insert(0, primary);
@ -286,7 +306,7 @@ pub(crate) async fn view_profile_diff_confirm_save_post(
VerifiedClientInformation(client_auth_info), VerifiedClientInformation(client_auth_info),
DomainInfo(domain_info), DomainInfo(domain_info),
) )
.await .await
{ {
Ok(_) => Ok(Redirect::to(Urls::Profile.as_ref()).into_response()), Ok(_) => Ok(Redirect::to(Urls::Profile.as_ref()).into_response()),
Err(e) => Ok(e.into_response()), Err(e) => Ok(e.into_response()),
@ -351,5 +371,5 @@ pub(crate) async fn view_profile_unlock_get(
Urls::Profile.as_ref(), Urls::Profile.as_ref(),
display_ctx, display_ctx,
) )
.await) .await)
} }

View file

@ -9,79 +9,87 @@ Profile Difference
(% block settings_window %) (% block settings_window %)
<form> <form>
(% if person.name != new_attrs.account_name %)
<input type="hidden" name="account_name" value="(( new_attrs.account_name ))"/> <input type="hidden" name="account_name" value="(( new_attrs.account_name ))"/>
(% endif %)
(% if person.displayname != new_attrs.display_name %)
<input type="hidden" name="display_name" value="(( new_attrs.display_name ))"/> <input type="hidden" name="display_name" value="(( new_attrs.display_name ))"/>
<!-- <input type="hidden" name="legal_name" value=" new_attrs.legal_name "/>--> (% endif %)
(% for email in new_attrs.emails %) (% for email in new_attrs.emails %)
(% if !email.primary %) (% if !email.primary %)
<input type="hidden" name="emails[]" value="(( email.value ))"/> <input type="hidden" name="emails[]" value="(( email.value ))"/>
(% endif %) (% endif %)
(% endfor %) (% endfor %)
(% if let Some(new_primary_mail) = new_primary_mail %) (% if let Some(new_primary_mail) = new_primary_mail %)
<input type="hidden" name="new_primary_mail" value="(( new_primary_mail ))"/> <input type="hidden" name="new_primary_mail" value="(( new_primary_mail ))"/>
(% endif %) (% endif %)
<table class="table table-bordered overflow-x-scroll"> <table class="table table-bordered overflow-x-scroll">
<thead> <thead>
<tr> <tr>
<th scope="col">Attribute</th> <th scope="col">Attribute</th>
<th scope="col">Old value</th> <th scope="col">Old value</th>
<th scope="col">New value</th> <th scope="col">New value</th>
</tr> </tr>
</thead> </thead>
(% if person.name != new_attrs.account_name %) (% if person.name != new_attrs.account_name %)
<tr> <tr>
<th scope="row">Username</th> <th scope="row">Username</th>
<td class="text-break">(( person.name ))</td> <td class="text-break">(( person.name ))</td>
<td class="text-break">(( new_attrs.account_name ))</td> <td class="text-break">(( new_attrs.account_name ))</td>
</tr> </tr>
(% endif %) (% endif %)
(% if person.displayname != new_attrs.display_name %) (% if person.displayname != new_attrs.display_name %)
<tr> <tr>
<th scope="row">Display name</th> <th scope="row">Display name</th>
<td class="text-break">(( person.displayname ))</td> <td class="text-break">(( person.displayname ))</td>
<td class="text-break">(( new_attrs.display_name ))</td> <td class="text-break">(( new_attrs.display_name ))</td>
</tr> </tr>
(% endif %) (% endif %)
<!-- TODO: List new items with +, same items with . --> (% if !emails_are_same %)
<tr> <tr>
<th scope="row">Primary Emails</th> <th scope="row">Primary Emails</th>
<td class="text-nowrap"> <td class="text-nowrap">
(( primary_mail.clone().unwrap_or("none".to_string()) )) (( primary_mail.clone().unwrap_or("none".to_string()) ))
</td> </td>
<td class="text-nowrap"> <td class="text-nowrap">
(( new_primary_mail.clone().unwrap_or("none".to_string()) )) (( new_primary_mail.clone().unwrap_or("none".to_string()) ))
</td> </td>
</tr> </tr>
<tr> <tr>
<th scope="row">Secondary Emails</th> <th scope="row">Secondary Emails</th>
<td class="text-break"> <td class="text-break">
<ul class="ps-3"> <ul class="ps-3">
(% for email in person.mails %) (% for email in person.mails %)
(% if !email.primary %) (% if !email.primary %)
<li class="text-nowrap">(( email.value ))</li> <li class="text-nowrap">(( email.value ))</li>
(% endif %) (% endif %)
(% endfor %) (% endfor %)
</ul> </ul>
</td> </td>
<td class="text-break"> <td class="text-break">
<ul class="ps-3"> <ul class="ps-3">
(% for email in new_attrs.emails %) (% for email in new_attrs.emails %)
(% if !email.primary %) (% if !email.primary %)
<li class="text-nowrap">(( email.value ))</li> <li class="text-nowrap">(( email.value ))</li>
(% endif %) (% endif %)
(% endfor %) (% endfor %)
</ul> </ul>
</td> </td>
</tr> </tr>
(% endif %)
</table> </table>
<!-- Edit button -->
<div class="pt-4" hx-target="#user_settings_container" hx-swap="outerHTML"> <div class="pt-4" hx-target="#user_settings_container" hx-swap="outerHTML">
(% if can_rw %) (% if can_rw %)
<button class="btn btn-danger" type="button" hx-get="/ui/profile" hx-target="body" hx-swap="outerHTML">Discard Changes</button> <button class="btn btn-danger" type="button" hx-get="/ui/profile" hx-target="body" hx-swap="outerHTML">Discard
<button class="btn btn-primary" type="button" hx-post="/ui/api/user_settings/confirm_profile" hx-target="body" hx-swap="outerHTML">Confirm Changes</button> Changes
</button>
<button class="btn btn-primary" type="button" hx-post="/ui/api/user_settings/confirm_profile" hx-target="body"
hx-swap="outerHTML">Confirm Changes
</button>
(% else %) (% else %)
<a href="/ui/profile/unlock" hx-boost="false"> <a href="/ui/profile/unlock" hx-boost="false">
<!-- TODO: at the moment, session expiring here means progress is lost. Do we just show an error screen ? We can't pass the update state through the reauth session, and we don't have profile update sessions like cred update. --> <!-- TODO: at the moment, session expiring here means progress is lost. Do we just show an error screen ? We can't pass the update state through the reauth session, and we don't have profile update sessions like cred update. -->