mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 12:37:00 +01:00
Credentials page/Self cred update flow UI improvements (#3012)
This commit is contained in:
parent
95fc6fc5bf
commit
72393996a7
|
@ -140,9 +140,9 @@ pub fn get_js_files(role: ServerRole) -> Result<JavaScriptFiles, ()> {
|
|||
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 {
|
||||
|
|
10
server/core/src/https/views/constants.rs
Normal file
10
server/core/src/https/views/constants.rs
Normal 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,
|
||||
}
|
|
@ -17,6 +17,7 @@ use crate::https::{
|
|||
};
|
||||
|
||||
mod apps;
|
||||
mod constants;
|
||||
mod cookies;
|
||||
mod errors;
|
||||
mod login;
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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<Response> {
|
||||
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<CUSessionToken, Response> {
|
||||
|
|
4
server/core/static/img/icons/building-lock.svg
Normal file
4
server/core/static/img/icons/building-lock.svg
Normal 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 |
4
server/core/static/img/icons/key.svg
Normal file
4
server/core/static/img/icons/key.svg
Normal 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 |
3
server/core/static/img/icons/person.svg
Normal file
3
server/core/static/img/icons/person.svg
Normal 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 |
4
server/core/static/img/icons/shield-lock.svg
Normal file
4
server/core/static/img/icons/shield-lock.svg
Normal 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 |
|
@ -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();
|
||||
});
|
||||
}
|
||||
})()
|
|
@ -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;
|
||||
|
|
|
@ -2,8 +2,6 @@
|
|||
(% block title %)Credentials Reset(% endblock %)
|
||||
|
||||
(% block head %)
|
||||
<script src="/pkg/external/cred_update.js"></script>
|
||||
<script src="/pkg/external/base64.js" async></script>
|
||||
(% endblock %)
|
||||
|
||||
(% block body %)
|
||||
|
|
15
server/core/templates/credentials_status.html
Normal file
15
server/core/templates/credentials_status.html
Normal 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 %)
|
|
@ -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)">
|
||||
<form class="needs-validation" novalidate>
|
||||
(% match ext_cred_portal %)
|
||||
|
|
|
@ -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">
|
||||
<div class="d-flex flex-sm-row flex-column">
|
||||
<div class="list-group side-menu">
|
||||
<a href="#" class="list-group-item list-group-item-action active">
|
||||
<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">
|
||||
<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>
|
||||
<div class="list-group side-menu flex-shrink-0">
|
||||
(% 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 %)
|
||||
<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">
|
||||
<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>
|
||||
(% call side_menu_item("UNIX Password", "/ui/update_credentials", ProfileMenuItems::UnixPassword, "building-lock") %)
|
||||
(% endif %)
|
||||
|
||||
<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>
|
||||
(% call side_menu_item("Credentials", "/ui/update_credentials", ProfileMenuItems::Credentials, "shield-lock") %)
|
||||
</div>
|
||||
<div id="settings-window" class="flex-grow-1 ps-sm-4 pt-sm-0 pt-4">
|
||||
<div class="(( crate::https::ui::CSS_PAGE_HEADER ))">
|
||||
|
@ -29,4 +24,4 @@
|
|||
(% endblock %)
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</main>
|
Loading…
Reference in a new issue