mirror of
https://github.com/kanidm/kanidm.git
synced 2025-05-07 01:25:06 +02:00
336 lines
10 KiB
Rust
336 lines
10 KiB
Rust
use super::constants::{ProfileMenuItems, Urls};
|
|
use super::errors::HtmxError;
|
|
use super::login::{LoginDisplayCtx, Reauth, ReauthPurpose};
|
|
use super::navbar::NavbarCtx;
|
|
use crate::https::errors::WebError;
|
|
use crate::https::extractors::{DomainInfo, VerifiedClientInformation};
|
|
use crate::https::middleware::KOpId;
|
|
use crate::https::ServerState;
|
|
use askama::Template;
|
|
use askama_axum::IntoResponse;
|
|
use axum::extract::{Query, State};
|
|
use axum::http::Uri;
|
|
use axum::response::Response;
|
|
use axum::Extension;
|
|
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;
|
|
use serde::Deserialize;
|
|
use serde::Serialize;
|
|
use std::fmt;
|
|
use std::fmt::Display;
|
|
use std::fmt::Formatter;
|
|
|
|
#[derive(Template)]
|
|
#[template(path = "user_settings.html")]
|
|
pub(crate) struct ProfileView {
|
|
navbar_ctx: NavbarCtx,
|
|
profile_partial: ProfilePartialView,
|
|
}
|
|
|
|
#[derive(Template, Clone)]
|
|
#[template(path = "user_settings_profile_partial.html")]
|
|
struct ProfilePartialView {
|
|
menu_active_item: ProfileMenuItems,
|
|
can_rw: bool,
|
|
person: ScimPerson,
|
|
scim_effective_access: ScimEffectiveAccess,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
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, 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 {
|
|
menu_active_item: ProfileMenuItems,
|
|
can_rw: bool,
|
|
person: ScimPerson,
|
|
new_attrs: ProfileAttributes,
|
|
}
|
|
|
|
#[derive(Template, Clone)]
|
|
#[template(path = "user_settings/form_email_entry_partial.html")]
|
|
pub(crate) struct FormEmailEntryListPartial {
|
|
can_edit: bool,
|
|
value: String,
|
|
primary: bool,
|
|
index: u16,
|
|
}
|
|
|
|
impl Display for ProfileAttributes {
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
|
write!(f, "{self:?}")
|
|
}
|
|
}
|
|
|
|
pub(crate) async fn view_profile_get(
|
|
State(state): State<ServerState>,
|
|
Extension(kopid): Extension<KOpId>,
|
|
VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
|
|
DomainInfo(domain_info): DomainInfo,
|
|
) -> Result<ProfileView, WebError> {
|
|
let uat: UserAuthToken = state
|
|
.qe_r_ref
|
|
.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 time = time::OffsetDateTime::now_utc() + time::Duration::new(60, 0);
|
|
|
|
let can_rw = uat.purpose_readwrite_active(time);
|
|
|
|
Ok(ProfileView {
|
|
navbar_ctx: NavbarCtx { domain_info },
|
|
|
|
profile_partial: ProfilePartialView {
|
|
menu_active_item: ProfileMenuItems::UserProfile,
|
|
can_rw,
|
|
person: scim_person,
|
|
scim_effective_access,
|
|
},
|
|
})
|
|
}
|
|
|
|
pub(crate) async fn view_profile_diff_start_save_post(
|
|
State(state): State<ServerState>,
|
|
Extension(kopid): Extension<KOpId>,
|
|
VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
|
|
DomainInfo(domain_info): DomainInfo,
|
|
// Form must be the last parameter because it consumes the request body
|
|
Form(query): Form<SaveProfileQuery>,
|
|
) -> axum::response::Result<Response> {
|
|
let uat: UserAuthToken = state
|
|
.qe_r_ref
|
|
.handle_whoami_uat(client_auth_info.clone(), kopid.eventid)
|
|
.map_err(|op_err| HtmxError::new(&kopid, op_err, domain_info.clone()))
|
|
.await?;
|
|
|
|
let time = time::OffsetDateTime::now_utc() + time::Duration::new(60, 0);
|
|
let can_rw = uat.purpose_readwrite_active(time);
|
|
// TODO: A bit overkill to request scimEffectiveAccess here.
|
|
let (scim_person, _) = crate::https::views::admin::persons::get_person_info(
|
|
uat.uuid,
|
|
state,
|
|
&kopid,
|
|
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: ProfileAttributes {
|
|
account_name: query.account_name,
|
|
display_name: query.display_name,
|
|
emails: new_mails,
|
|
},
|
|
};
|
|
|
|
Ok((
|
|
HxPushUrl(Uri::from_static("/ui/profile/diff")),
|
|
profile_view,
|
|
)
|
|
.into_response())
|
|
}
|
|
|
|
pub(crate) async fn view_profile_diff_confirm_save_post(
|
|
State(state): State<ServerState>,
|
|
Extension(kopid): Extension<KOpId>,
|
|
VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
|
|
DomainInfo(domain_info): DomainInfo,
|
|
// Form must be the last parameter because it consumes the request body
|
|
Form(mut new_attrs): Form<ProfileAttributes>,
|
|
) -> axum::response::Result<Response> {
|
|
let uat: UserAuthToken = state
|
|
.qe_r_ref
|
|
.handle_whoami_uat(client_auth_info.clone(), kopid.eventid)
|
|
.map_err(|op_err| HtmxError::new(&kopid, op_err, domain_info.clone()))
|
|
.await?;
|
|
dbg!(&new_attrs);
|
|
|
|
let filter = filter_all!(f_and!([f_id(uat.uuid.to_string().as_str())]));
|
|
|
|
state
|
|
.qe_w_ref
|
|
.handle_setattribute(
|
|
client_auth_info.clone(),
|
|
uat.uuid.to_string(),
|
|
ATTR_DISPLAYNAME.to_string(),
|
|
vec![new_attrs.display_name],
|
|
filter.clone(),
|
|
kopid.eventid,
|
|
)
|
|
.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(),
|
|
email_addresses,
|
|
filter.clone(),
|
|
kopid.eventid,
|
|
)
|
|
.map_err(|op_err| HtmxError::new(&kopid, op_err, domain_info.clone()))
|
|
.await?;
|
|
|
|
// TODO: These are normally not permitted, user should be prevented from changing non modifiable fields in the UI though
|
|
// state
|
|
// .qe_w_ref
|
|
// .handle_setattribute(
|
|
// client_auth_info.clone(),
|
|
// uat.uuid.to_string(),
|
|
// ATTR_EMAIL.to_string(),
|
|
// vec![new_attrs.email.unwrap_or("".to_string())],
|
|
// filter.clone(),
|
|
// kopid.eventid,
|
|
// )
|
|
// .map_err(|op_err| HtmxError::new(&kopid, op_err))
|
|
// .await?;
|
|
//
|
|
// state
|
|
// .qe_w_ref
|
|
// .handle_setattribute(
|
|
// client_auth_info.clone(),
|
|
// uat.uuid.to_string(),
|
|
// ATTR_NAME.to_string(),
|
|
// vec![new_attrs.account_name],
|
|
// filter.clone(),
|
|
// kopid.eventid,
|
|
// )
|
|
// .map_err(|op_err| HtmxError::new(&kopid, op_err))
|
|
// .await?;
|
|
|
|
// TODO: Calling this here returns the old attributes
|
|
match view_profile_get(
|
|
State(state),
|
|
Extension(kopid),
|
|
VerifiedClientInformation(client_auth_info),
|
|
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,
|
|
FormEmailEntryListPartial {
|
|
can_edit: true,
|
|
value: "".to_string(),
|
|
primary: email_query.email_index.is_none(),
|
|
index: email_query.email_index.map(|i| i + 1).unwrap_or(0),
|
|
},
|
|
)
|
|
.into_response())
|
|
}
|
|
|
|
pub(crate) async fn view_profile_unlock_get(
|
|
State(state): State<ServerState>,
|
|
VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
|
|
DomainInfo(domain_info): DomainInfo,
|
|
Extension(kopid): Extension<KOpId>,
|
|
jar: CookieJar,
|
|
) -> Result<Response, HtmxError> {
|
|
let uat: UserAuthToken = state
|
|
.qe_r_ref
|
|
.handle_whoami_uat(client_auth_info.clone(), kopid.eventid)
|
|
.await
|
|
.map_err(|op_err| HtmxError::new(&kopid, op_err, domain_info.clone()))?;
|
|
|
|
let display_ctx = LoginDisplayCtx {
|
|
domain_info,
|
|
oauth2: None,
|
|
reauth: Some(Reauth {
|
|
username: uat.spn,
|
|
purpose: ReauthPurpose::ProfileSettings,
|
|
}),
|
|
error: None,
|
|
};
|
|
|
|
Ok(super::login::view_reauth_get(
|
|
state,
|
|
client_auth_info,
|
|
kopid,
|
|
jar,
|
|
Urls::Profile.as_ref(),
|
|
display_ctx,
|
|
)
|
|
.await)
|
|
}
|