Credentials page/Self cred update flow UI improvements (#3012)

This commit is contained in:
Wei Jian Gan 2024-09-07 12:56:58 +08:00 committed by GitHub
parent 95fc6fc5bf
commit 72393996a7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 131 additions and 34 deletions

View file

@ -140,9 +140,9 @@ pub fn get_js_files(role: ServerRole) -> Result<JavaScriptFiles, ()> {
vec![ vec![
("external/bootstrap.bundle.min.js", None, false, false), ("external/bootstrap.bundle.min.js", None, false, false),
("external/htmx.min.1.9.12.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/confetti.js", None, false, false),
("external/base64.js", None, false, false), ("external/base64.js", None, false, false),
("modules/cred_update.mjs", None, false, false),
("pkhtml.js", None, false, false), ("pkhtml.js", None, false, false),
] ]
} else { } else {

View file

@ -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,
}

View file

@ -17,6 +17,7 @@ use crate::https::{
}; };
mod apps; mod apps;
mod constants;
mod cookies; mod cookies;
mod errors; mod errors;
mod login; mod login;

View file

@ -13,6 +13,8 @@ use axum_htmx::{HxPushUrl, HxRequest};
use futures_util::TryFutureExt; use futures_util::TryFutureExt;
use kanidm_proto::internal::UserAuthToken; use kanidm_proto::internal::UserAuthToken;
use super::constants::ProfileMenuItems;
#[derive(Template)] #[derive(Template)]
#[template(path = "user_settings.html")] #[template(path = "user_settings.html")]
struct ProfileView { struct ProfileView {
@ -22,6 +24,7 @@ struct ProfileView {
#[derive(Template, Clone)] #[derive(Template, Clone)]
#[template(path = "user_settings_profile_partial.html")] #[template(path = "user_settings_profile_partial.html")]
struct ProfilePartialView { struct ProfilePartialView {
menu_active_item: ProfileMenuItems,
can_rw: bool, can_rw: bool,
account_name: String, account_name: String,
display_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 can_rw = uat.purpose_readwrite_active(time);
let profile_partial_view = ProfilePartialView { let profile_partial_view = ProfilePartialView {
menu_active_item: ProfileMenuItems::UserProfile,
can_rw, can_rw,
account_name: uat.name().to_string(), account_name: uat.name().to_string(),
display_name: uat.displayname.clone(), display_name: uat.displayname.clone(),

View file

@ -26,11 +26,18 @@ use kanidm_proto::internal::{
use crate::https::extractors::{DomainInfo, DomainInfoRead, VerifiedClientInformation}; use crate::https::extractors::{DomainInfo, DomainInfoRead, VerifiedClientInformation};
use crate::https::middleware::KOpId; use crate::https::middleware::KOpId;
use crate::https::views::constants::ProfileMenuItems;
use crate::https::views::errors::HtmxError; use crate::https::views::errors::HtmxError;
use crate::https::ServerState; use crate::https::ServerState;
use super::{HtmlTemplate, UnrecoverableErrorView}; use super::{HtmlTemplate, UnrecoverableErrorView};
#[derive(Template)]
#[template(path = "user_settings.html")]
struct ProfileView {
profile_partial: CredStatusView,
}
#[derive(Template)] #[derive(Template)]
#[template(path = "credentials_reset_form.html")] #[template(path = "credentials_reset_form.html")]
struct ResetCredFormView { struct ResetCredFormView {
@ -46,6 +53,16 @@ struct CredResetView {
credentials_update_partial: CredResetPartialView, 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)] #[derive(Template)]
#[template(path = "credentials_update_partial.html")] #[template(path = "credentials_update_partial.html")]
struct CredResetPartialView { struct CredResetPartialView {
@ -590,7 +607,7 @@ pub(crate) async fn view_self_reset_get(
.map_err(|op_err| HtmxError::new(&kopid, op_err)) .map_err(|op_err| HtmxError::new(&kopid, op_err))
.await?; .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); jar = add_cu_cookie(jar, &state, cu_session_token);
Ok((jar, cu_resp).into_response()) Ok((jar, cu_resp).into_response())
@ -631,6 +648,12 @@ pub(crate) async fn view_reset_get(
) -> axum::response::Result<Response> { ) -> axum::response::Result<Response> {
let push_url = HxPushUrl(Uri::from_static("/ui/reset")); let push_url = HxPushUrl(Uri::from_static("/ui/reset"));
let cookie = jar.get(COOKIE_CU_SESSION_TOKEN); 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 { if let Some(cookie) = cookie {
// We already have a session // We already have a session
let cu_session_token = cookie.value(); let cu_session_token = cookie.value();
@ -661,7 +684,7 @@ pub(crate) async fn view_reset_get(
}; };
// CU Session cookie is okay // 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) Ok(cu_resp)
} else if let Some(token) = params.token { } else if let Some(token) = params.token {
// We have a reset token and want to create a new session // We have a reset token and want to create a new session
@ -671,7 +694,7 @@ pub(crate) async fn view_reset_get(
.await .await
{ {
Ok((cu_session_token, cu_status)) => { 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); jar = add_cu_cookie(jar, &state, cu_session_token);
Ok((jar, cu_resp).into_response()) Ok((jar, cu_resp).into_response())
@ -736,21 +759,46 @@ fn get_cu_partial_response(cu_status: CUStatus) -> Response {
.into_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 spn = cu_status.spn.clone();
let displayname = cu_status.displayname.clone(); let displayname = cu_status.displayname.clone();
let (username, _domain) = spn.split_once('@').unwrap_or(("", &spn)); let (username, _domain) = spn.split_once('@').unwrap_or(("", &spn));
let names = format!("{} ({})", displayname, username); let names = format!("{} ({})", displayname, username);
let credentials_update_partial = get_cu_partial(cu_status); let credentials_update_partial = get_cu_partial(cu_status);
(
HxPushUrl(Uri::from_static("/ui/reset")), if is_logged_in {
HtmlTemplate(CredResetView { let cred_status_view = CredStatusView {
menu_active_item: ProfileMenuItems::Credentials,
domain_info, domain_info,
names, names,
credentials_update_partial, credentials_update_partial,
}), // TODO: fill in posix enabled
) posix_enabled: false,
.into_response() };
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<CUSessionToken, Response> { async fn get_cu_session(jar: CookieJar) -> Result<CUSessionToken, Response> {

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-building-lock" viewBox="0 0 16 16">
<path d="M2 1a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v6.5a.5.5 0 0 1-1 0V1H3v14h3v-2.5a.5.5 0 0 1 .5-.5H8v4H3a1 1 0 0 1-1-1z"/>
<path d="M4.5 2a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5zm2.5.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5zm3.5-.5a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5zM4 5.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5zM7.5 5a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5zm2.5.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5zM4.5 8a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5zm2.5.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5zM9 13a1 1 0 0 1 1-1v-1a2 2 0 1 1 4 0v1a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-4a1 1 0 0 1-1-1zm3-3a1 1 0 0 0-1 1v1h2v-1a1 1 0 0 0-1-1"/>
</svg>

After

Width:  |  Height:  |  Size: 1 KiB

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-key" viewBox="0 0 16 16">
<path d="M0 8a4 4 0 0 1 7.465-2H14a.5.5 0 0 1 .354.146l1.5 1.5a.5.5 0 0 1 0 .708l-1.5 1.5a.5.5 0 0 1-.708 0L13 9.207l-.646.647a.5.5 0 0 1-.708 0L11 9.207l-.646.647a.5.5 0 0 1-.708 0L9 9.207l-.646.647A.5.5 0 0 1 8 10h-.535A4 4 0 0 1 0 8m4-3a3 3 0 1 0 2.712 4.285A.5.5 0 0 1 7.163 9h.63l.853-.854a.5.5 0 0 1 .708 0l.646.647.646-.647a.5.5 0 0 1 .708 0l.646.647.646-.647a.5.5 0 0 1 .708 0l.646.647.793-.793-1-1h-6.63a.5.5 0 0 1-.451-.285A3 3 0 0 0 4 5"/>
<path d="M4 8a1 1 0 1 1-2 0 1 1 0 0 1 2 0"/>
</svg>

After

Width:  |  Height:  |  Size: 628 B

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-person" viewBox="0 0 16 16">
<path d="M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6m2-3a2 2 0 1 1-4 0 2 2 0 0 1 4 0m4 8c0 1-1 1-1 1H3s-1 0-1-1 1-4 6-4 6 3 6 4m-1-.004c-.001-.246-.154-.986-.832-1.664C11.516 10.68 10.289 10 8 10s-3.516.68-4.168 1.332c-.678.678-.83 1.418-.832 1.664z"/>
</svg>

After

Width:  |  Height:  |  Size: 375 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-shield-lock" viewBox="0 0 16 16">
<path d="M5.338 1.59a61 61 0 0 0-2.837.856.48.48 0 0 0-.328.39c-.554 4.157.726 7.19 2.253 9.188a10.7 10.7 0 0 0 2.287 2.233c.346.244.652.42.893.533q.18.085.293.118a1 1 0 0 0 .101.025 1 1 0 0 0 .1-.025q.114-.034.294-.118c.24-.113.547-.29.893-.533a10.7 10.7 0 0 0 2.287-2.233c1.527-1.997 2.807-5.031 2.253-9.188a.48.48 0 0 0-.328-.39c-.651-.213-1.75-.56-2.837-.855C9.552 1.29 8.531 1.067 8 1.067c-.53 0-1.552.223-2.662.524zM5.072.56C6.157.265 7.31 0 8 0s1.843.265 2.928.56c1.11.3 2.229.655 2.887.87a1.54 1.54 0 0 1 1.044 1.262c.596 4.477-.787 7.795-2.465 9.99a11.8 11.8 0 0 1-2.517 2.453 7 7 0 0 1-1.048.625c-.28.132-.581.24-.829.24s-.548-.108-.829-.24a7 7 0 0 1-1.048-.625 11.8 11.8 0 0 1-2.517-2.453C1.928 10.487.545 7.169 1.141 2.692A1.54 1.54 0 0 1 2.185 1.43 63 63 0 0 1 5.072.56"/>
<path d="M9.5 6.5a1.5 1.5 0 0 1-1 1.415l.385 1.99a.5.5 0 0 1-.491.595h-.788a.5.5 0 0 1-.49-.595l.384-1.99a1.5 1.5 0 1 1 2-1.415"/>
</svg>

After

Width:  |  Height:  |  Size: 1 KiB

View file

@ -1,3 +1,5 @@
console.debug('credupdate: loaded');
// Makes the password form interactive (e.g. shows when passwords don't match) // Makes the password form interactive (e.g. shows when passwords don't match)
function setupInteractivePwdFormListeners() { function setupInteractivePwdFormListeners() {
const new_pwd = document.getElementById("new-password"); 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) { if (event.detail.xhr.status === 422 || event.detail.xhr.status === 500) {
console.log("Still swapping failure response") console.log("Still swapping failure response")
event.detail.shouldSwap = true; event.detail.shouldSwap = true;
@ -117,11 +119,12 @@ function updateSubmitButtonVisibility(event) {
submitButton.disabled = event.value === ""; submitButton.disabled = event.value === "";
} }
window.onload = function () { (function() {
console.debug('credupdate: init');
document.body.addEventListener("addPasswordSwapped", () => { setupInteractivePwdFormListeners() }); document.body.addEventListener("addPasswordSwapped", () => { setupInteractivePwdFormListeners() });
document.body.addEventListener("addPasskeySwapped", () => { document.body.addEventListener("addPasskeySwapped", () => {
setupPasskeyNamingSafariButton(); setupPasskeyNamingSafariButton();
startPasskeyEnrollment(); startPasskeyEnrollment();
setupSubmitBtnVisibility(); setupSubmitBtnVisibility();
}); });
} })()

View file

@ -14,6 +14,11 @@ body {
margin: auto; margin: auto;
} }
#settings-window:has(.form-cred-reset-body) .form-cred-reset-body {
max-width: unset;
padding: unset;
}
.form-signin-body { .form-signin-body {
display: flex; display: flex;
align-items: center; align-items: center;

View file

@ -2,8 +2,6 @@
(% block title %)Credentials Reset(% endblock %) (% block title %)Credentials Reset(% endblock %)
(% block head %) (% block head %)
<script src="/pkg/external/cred_update.js"></script>
<script src="/pkg/external/base64.js" async></script>
(% endblock %) (% endblock %)
(% block body %) (% block body %)

View file

@ -0,0 +1,15 @@
(% extends "user_settings_partial_base.html" %)
(% block selected_setting_group %)Credentials(% endblock %)
(% block settings_window %)
<div class="d-flex align-items-start form-cred-reset-body">
<div class="w-100">
<div class="py-3">
<p>(( names ))</p>
<p>(( domain_info.display_name() ))</p>
</div>
(( credentials_update_partial|safe ))
</div>
</div>
(% endblock %)

View file

@ -1,3 +1,6 @@
<script type="module" src="/pkg/modules/cred_update.mjs" async></script>
<script src="/pkg/external/base64.js" async></script>
<div class="row g-3" id="credentialUpdateDynamicSection" hx-on::before-swap="stillSwapFailureResponse(event)"> <div class="row g-3" id="credentialUpdateDynamicSection" hx-on::before-swap="stillSwapFailureResponse(event)">
<form class="needs-validation" novalidate> <form class="needs-validation" novalidate>
(% match ext_cred_portal %) (% match ext_cred_portal %)

View file

@ -1,24 +1,19 @@
(% macro side_menu_item(label, href, menu_item, icon_name) %)
<a hx-select="main" hx-target="main" hx-swap="outerHTML show:false" href="(( href ))"
class="list-group-item list-group-item-action d-flex (% if menu_active_item == menu_item %) active(% endif %)">
<img class="me-3" src="/pkg/img/icons/(( icon_name )).svg" alt="">(( label ))
</a>
(% endmacro %)
<main class="container-xxl pb-5"> <main class="container-xxl pb-5">
<div class="d-flex flex-sm-row flex-column"> <div class="d-flex flex-sm-row flex-column">
<div class="list-group side-menu"> <div class="list-group side-menu flex-shrink-0">
<a href="#" class="list-group-item list-group-item-action active"> (% call side_menu_item("Profile", "/ui/profile", ProfileMenuItems::UserProfile, "person") %)
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-person-fill me-3" viewBox="0 0 16 16"> (% call side_menu_item("SSH Keys", "/ui/ssh_keys", ProfileMenuItems::SshKeys, "key") %)
<path d="M3 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1zm5-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6"/>
</svg>Profile</a>
<a href="#" class="list-group-item list-group-item-action">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-key-fill me-3" viewBox="0 0 16 16">
<path d="M3.5 11.5a3.5 3.5 0 1 1 3.163-5H14L15.5 8 14 9.5l-1-1-1 1-1-1-1 1-1-1-1 1H6.663a3.5 3.5 0 0 1-3.163 2M2.5 9a1 1 0 1 0 0-2 1 1 0 0 0 0 2"/>
</svg>SSH Keys</a>
(% if posix_enabled %) (% if posix_enabled %)
<a href="#" class="list-group-item list-group-item-action"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-building-fill-lock me-3" viewBox="0 0 16 16"> (% call side_menu_item("UNIX Password", "/ui/update_credentials", ProfileMenuItems::UnixPassword, "building-lock") %)
<path d="M2 1a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v7.764a3 3 0 0 0-4.989 2.497 2 2 0 0 0-.743.739H6.5a.5.5 0 0 0-.5.5V16H3a1 1 0 0 1-1-1zm2 1.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5m3 0v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5m3.5-.5a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5zM4 5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5M7.5 5a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5zm2.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5M4.5 8a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5zm2.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5"/>
<path d="M9 13a1 1 0 0 1 1-1v-1a2 2 0 1 1 4 0v1a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-4a1 1 0 0 1-1-1zm3-3a1 1 0 0 0-1 1v1h2v-1a1 1 0 0 0-1-1"/>
</svg>UNIX Password</a>
(% endif %) (% endif %)
(% call side_menu_item("Credentials", "/ui/update_credentials", ProfileMenuItems::Credentials, "shield-lock") %)
<a href="/ui/update_credentials" class="list-group-item list-group-item-action"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-shield-fill-plus me-3" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M8 0c-.69 0-1.843.265-2.928.56-1.11.3-2.229.655-2.887.87a1.54 1.54 0 0 0-1.044 1.262c-.596 4.477.787 7.795 2.465 9.99a11.8 11.8 0 0 0 2.517 2.453c.386.273.744.482 1.048.625.28.132.581.24.829.24s.548-.108.829-.24a7 7 0 0 0 1.048-.625 11.8 11.8 0 0 0 2.517-2.453c1.678-2.195 3.061-5.513 2.465-9.99a1.54 1.54 0 0 0-1.044-1.263 63 63 0 0 0-2.887-.87C9.843.266 8.69 0 8 0m-.5 5a.5.5 0 0 1 1 0v1.5H10a.5.5 0 0 1 0 1H8.5V9a.5.5 0 0 1-1 0V7.5H6a.5.5 0 0 1 0-1h1.5z"/>
</svg>Add another device</a>
</div> </div>
<div id="settings-window" class="flex-grow-1 ps-sm-4 pt-sm-0 pt-4"> <div id="settings-window" class="flex-grow-1 ps-sm-4 pt-sm-0 pt-4">
<div class="(( crate::https::ui::CSS_PAGE_HEADER ))"> <div class="(( crate::https::ui::CSS_PAGE_HEADER ))">
@ -29,4 +24,4 @@
(% endblock %) (% endblock %)
</div> </div>
</div> </div>
</main> </main>