From 72393996a72115c855cc90a30098ed95d740b998 Mon Sep 17 00:00:00 2001 From: Wei Jian Gan Date: Sat, 7 Sep 2024 12:56:58 +0800 Subject: [PATCH] Credentials page/Self cred update flow UI improvements (#3012) --- server/core/src/https/mod.rs | 2 +- server/core/src/https/views/constants.rs | 10 +++ server/core/src/https/views/mod.rs | 1 + server/core/src/https/views/profile.rs | 4 ++ server/core/src/https/views/reset.rs | 68 ++++++++++++++++--- .../core/static/img/icons/building-lock.svg | 4 ++ server/core/static/img/icons/key.svg | 4 ++ server/core/static/img/icons/person.svg | 3 + server/core/static/img/icons/shield-lock.svg | 4 ++ .../cred_update.mjs} | 9 ++- server/core/static/style.css | 5 ++ server/core/templates/credentials_reset.html | 2 - server/core/templates/credentials_status.html | 15 ++++ .../templates/credentials_update_partial.html | 3 + .../templates/user_settings_partial_base.html | 31 ++++----- 15 files changed, 131 insertions(+), 34 deletions(-) create mode 100644 server/core/src/https/views/constants.rs create mode 100644 server/core/static/img/icons/building-lock.svg create mode 100644 server/core/static/img/icons/key.svg create mode 100644 server/core/static/img/icons/person.svg create mode 100644 server/core/static/img/icons/shield-lock.svg rename server/core/static/{external/cred_update.js => modules/cred_update.mjs} (97%) create mode 100644 server/core/templates/credentials_status.html diff --git a/server/core/src/https/mod.rs b/server/core/src/https/mod.rs index 048c6f06e..4c4f0fc68 100644 --- a/server/core/src/https/mod.rs +++ b/server/core/src/https/mod.rs @@ -140,9 +140,9 @@ pub fn get_js_files(role: ServerRole) -> Result { vec![ ("external/bootstrap.bundle.min.js", None, false, false), ("external/htmx.min.1.9.12.js", None, false, false), - ("external/cred_update.js", None, false, false), ("external/confetti.js", None, false, false), ("external/base64.js", None, false, false), + ("modules/cred_update.mjs", None, false, false), ("pkhtml.js", None, false, false), ] } else { diff --git a/server/core/src/https/views/constants.rs b/server/core/src/https/views/constants.rs new file mode 100644 index 000000000..3d24658e8 --- /dev/null +++ b/server/core/src/https/views/constants.rs @@ -0,0 +1,10 @@ +use serde::{Deserialize, Serialize}; + +#[derive(PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "snake_case")] +pub(crate) enum ProfileMenuItems { + UserProfile, + SshKeys, + Credentials, + UnixPassword, +} diff --git a/server/core/src/https/views/mod.rs b/server/core/src/https/views/mod.rs index 3cca4b4a7..603564309 100644 --- a/server/core/src/https/views/mod.rs +++ b/server/core/src/https/views/mod.rs @@ -17,6 +17,7 @@ use crate::https::{ }; mod apps; +mod constants; mod cookies; mod errors; mod login; diff --git a/server/core/src/https/views/profile.rs b/server/core/src/https/views/profile.rs index 1c201d275..df8d41791 100644 --- a/server/core/src/https/views/profile.rs +++ b/server/core/src/https/views/profile.rs @@ -13,6 +13,8 @@ use axum_htmx::{HxPushUrl, HxRequest}; use futures_util::TryFutureExt; use kanidm_proto::internal::UserAuthToken; +use super::constants::ProfileMenuItems; + #[derive(Template)] #[template(path = "user_settings.html")] struct ProfileView { @@ -22,6 +24,7 @@ struct ProfileView { #[derive(Template, Clone)] #[template(path = "user_settings_profile_partial.html")] struct ProfilePartialView { + menu_active_item: ProfileMenuItems, can_rw: bool, account_name: String, display_name: String, @@ -47,6 +50,7 @@ pub(crate) async fn view_profile_get( let can_rw = uat.purpose_readwrite_active(time); let profile_partial_view = ProfilePartialView { + menu_active_item: ProfileMenuItems::UserProfile, can_rw, account_name: uat.name().to_string(), display_name: uat.displayname.clone(), diff --git a/server/core/src/https/views/reset.rs b/server/core/src/https/views/reset.rs index cbbba56af..46f8dbbdc 100644 --- a/server/core/src/https/views/reset.rs +++ b/server/core/src/https/views/reset.rs @@ -26,11 +26,18 @@ use kanidm_proto::internal::{ use crate::https::extractors::{DomainInfo, DomainInfoRead, VerifiedClientInformation}; use crate::https::middleware::KOpId; +use crate::https::views::constants::ProfileMenuItems; use crate::https::views::errors::HtmxError; use crate::https::ServerState; use super::{HtmlTemplate, UnrecoverableErrorView}; +#[derive(Template)] +#[template(path = "user_settings.html")] +struct ProfileView { + profile_partial: CredStatusView, +} + #[derive(Template)] #[template(path = "credentials_reset_form.html")] struct ResetCredFormView { @@ -46,6 +53,16 @@ struct CredResetView { credentials_update_partial: CredResetPartialView, } +#[derive(Template)] +#[template(path = "credentials_status.html")] +struct CredStatusView { + domain_info: DomainInfoRead, + menu_active_item: ProfileMenuItems, + names: String, + credentials_update_partial: CredResetPartialView, + posix_enabled: bool, +} + #[derive(Template)] #[template(path = "credentials_update_partial.html")] struct CredResetPartialView { @@ -590,7 +607,7 @@ pub(crate) async fn view_self_reset_get( .map_err(|op_err| HtmxError::new(&kopid, op_err)) .await?; - let cu_resp = get_cu_response(domain_info, cu_status); + let cu_resp = get_cu_response(domain_info, cu_status, true); jar = add_cu_cookie(jar, &state, cu_session_token); Ok((jar, cu_resp).into_response()) @@ -631,6 +648,12 @@ pub(crate) async fn view_reset_get( ) -> axum::response::Result { let push_url = HxPushUrl(Uri::from_static("/ui/reset")); let cookie = jar.get(COOKIE_CU_SESSION_TOKEN); + let is_logged_in = state + .qe_r_ref + .handle_auth_valid(_client_auth_info.clone(), kopid.eventid) + .await + .is_ok(); + if let Some(cookie) = cookie { // We already have a session let cu_session_token = cookie.value(); @@ -661,7 +684,7 @@ pub(crate) async fn view_reset_get( }; // CU Session cookie is okay - let cu_resp = get_cu_response(domain_info, cu_status); + let cu_resp = get_cu_response(domain_info, cu_status, is_logged_in); Ok(cu_resp) } else if let Some(token) = params.token { // We have a reset token and want to create a new session @@ -671,7 +694,7 @@ pub(crate) async fn view_reset_get( .await { Ok((cu_session_token, cu_status)) => { - let cu_resp = get_cu_response(domain_info, cu_status); + let cu_resp = get_cu_response(domain_info, cu_status, is_logged_in); jar = add_cu_cookie(jar, &state, cu_session_token); Ok((jar, cu_resp).into_response()) @@ -736,21 +759,46 @@ fn get_cu_partial_response(cu_status: CUStatus) -> Response { .into_response() } -fn get_cu_response(domain_info: DomainInfoRead, cu_status: CUStatus) -> Response { +fn get_cu_response( + domain_info: DomainInfoRead, + cu_status: CUStatus, + is_logged_in: bool, +) -> Response { let spn = cu_status.spn.clone(); let displayname = cu_status.displayname.clone(); let (username, _domain) = spn.split_once('@').unwrap_or(("", &spn)); let names = format!("{} ({})", displayname, username); let credentials_update_partial = get_cu_partial(cu_status); - ( - HxPushUrl(Uri::from_static("/ui/reset")), - HtmlTemplate(CredResetView { + + if is_logged_in { + let cred_status_view = CredStatusView { + menu_active_item: ProfileMenuItems::Credentials, domain_info, names, credentials_update_partial, - }), - ) - .into_response() + // TODO: fill in posix enabled + posix_enabled: false, + }; + let profile_view = ProfileView { + profile_partial: cred_status_view, + }; + + ( + HxPushUrl(Uri::from_static("/ui/update_credentials")), + HtmlTemplate(profile_view), + ) + .into_response() + } else { + ( + HxPushUrl(Uri::from_static("/ui/reset")), + HtmlTemplate(CredResetView { + domain_info, + names, + credentials_update_partial, + }), + ) + .into_response() + } } async fn get_cu_session(jar: CookieJar) -> Result { diff --git a/server/core/static/img/icons/building-lock.svg b/server/core/static/img/icons/building-lock.svg new file mode 100644 index 000000000..591a2e963 --- /dev/null +++ b/server/core/static/img/icons/building-lock.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/server/core/static/img/icons/key.svg b/server/core/static/img/icons/key.svg new file mode 100644 index 000000000..b0d1e16d1 --- /dev/null +++ b/server/core/static/img/icons/key.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/server/core/static/img/icons/person.svg b/server/core/static/img/icons/person.svg new file mode 100644 index 000000000..98ea060fe --- /dev/null +++ b/server/core/static/img/icons/person.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/server/core/static/img/icons/shield-lock.svg b/server/core/static/img/icons/shield-lock.svg new file mode 100644 index 000000000..316fb3c03 --- /dev/null +++ b/server/core/static/img/icons/shield-lock.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/server/core/static/external/cred_update.js b/server/core/static/modules/cred_update.mjs similarity index 97% rename from server/core/static/external/cred_update.js rename to server/core/static/modules/cred_update.mjs index 44f1d7ab4..22605d69e 100644 --- a/server/core/static/external/cred_update.js +++ b/server/core/static/modules/cred_update.mjs @@ -1,3 +1,5 @@ +console.debug('credupdate: loaded'); + // Makes the password form interactive (e.g. shows when passwords don't match) function setupInteractivePwdFormListeners() { const new_pwd = document.getElementById("new-password"); @@ -41,7 +43,7 @@ function setupInteractivePwdFormListeners() { }); } -function stillSwapFailureResponse(event) { +window.stillSwapFailureResponse = function(event) { if (event.detail.xhr.status === 422 || event.detail.xhr.status === 500) { console.log("Still swapping failure response") event.detail.shouldSwap = true; @@ -117,11 +119,12 @@ function updateSubmitButtonVisibility(event) { submitButton.disabled = event.value === ""; } -window.onload = function () { +(function() { + console.debug('credupdate: init'); document.body.addEventListener("addPasswordSwapped", () => { setupInteractivePwdFormListeners() }); document.body.addEventListener("addPasskeySwapped", () => { setupPasskeyNamingSafariButton(); startPasskeyEnrollment(); setupSubmitBtnVisibility(); }); -} +})() diff --git a/server/core/static/style.css b/server/core/static/style.css index c2d62c328..c2c71faf2 100644 --- a/server/core/static/style.css +++ b/server/core/static/style.css @@ -14,6 +14,11 @@ body { margin: auto; } +#settings-window:has(.form-cred-reset-body) .form-cred-reset-body { + max-width: unset; + padding: unset; +} + .form-signin-body { display: flex; align-items: center; diff --git a/server/core/templates/credentials_reset.html b/server/core/templates/credentials_reset.html index f34c4bdb0..21006c631 100644 --- a/server/core/templates/credentials_reset.html +++ b/server/core/templates/credentials_reset.html @@ -2,8 +2,6 @@ (% block title %)Credentials Reset(% endblock %) (% block head %) - - (% endblock %) (% block body %) diff --git a/server/core/templates/credentials_status.html b/server/core/templates/credentials_status.html new file mode 100644 index 000000000..7e017965f --- /dev/null +++ b/server/core/templates/credentials_status.html @@ -0,0 +1,15 @@ +(% extends "user_settings_partial_base.html" %) +(% block selected_setting_group %)Credentials(% endblock %) + + +(% block settings_window %) +
+
+
+

(( names ))

+

(( domain_info.display_name() ))

+
+ (( credentials_update_partial|safe )) +
+
+(% endblock %) diff --git a/server/core/templates/credentials_update_partial.html b/server/core/templates/credentials_update_partial.html index 1f13b6ec2..81f8acfc8 100644 --- a/server/core/templates/credentials_update_partial.html +++ b/server/core/templates/credentials_update_partial.html @@ -1,3 +1,6 @@ + + +
(% match ext_cred_portal %) diff --git a/server/core/templates/user_settings_partial_base.html b/server/core/templates/user_settings_partial_base.html index 244838deb..fc2a5ed28 100644 --- a/server/core/templates/user_settings_partial_base.html +++ b/server/core/templates/user_settings_partial_base.html @@ -1,24 +1,19 @@ +(% macro side_menu_item(label, href, menu_item, icon_name) %) + + (( label )) + +(% endmacro %) +
-
- - - - Profile - - - - SSH Keys +
+ (% call side_menu_item("Profile", "/ui/profile", ProfileMenuItems::UserProfile, "person") %) + (% call side_menu_item("SSH Keys", "/ui/ssh_keys", ProfileMenuItems::SshKeys, "key") %) (% if posix_enabled %) - - - - UNIX Password + (% call side_menu_item("UNIX Password", "/ui/update_credentials", ProfileMenuItems::UnixPassword, "building-lock") %) (% endif %) - - - - Add another device + (% call side_menu_item("Credentials", "/ui/update_credentials", ProfileMenuItems::Credentials, "shield-lock") %)
@@ -29,4 +24,4 @@ (% endblock %)
-
+ \ No newline at end of file