mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 12:37:00 +01:00
Harmonize UI and remove unused css (#3033)
------- Co-authored-by: Wei Jian Gan <wg@danicapension.dk> Co-authored-by: William Brown <william@blackhats.net.au>
This commit is contained in:
parent
151a9ad90f
commit
bc55313d87
|
@ -9,12 +9,9 @@ use super::ServerState;
|
|||
|
||||
use crate::https::extractors::{DomainInfo, DomainInfoRead};
|
||||
|
||||
// when you want to put big text at the top of the page
|
||||
pub const CSS_PAGE_HEADER: &str = "d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-0 pb-0 mb-3 border-bottom";
|
||||
|
||||
pub const CSS_NAVBAR_NAV: &str = "navbar navbar-expand-md navbar-dark bg-dark mb-4";
|
||||
pub const CSS_NAVBAR_BRAND: &str = "navbar-brand navbar-dark";
|
||||
pub const CSS_NAVBAR_LINKS_UL: &str = "navbar-nav me-auto mb-2 mb-md-0";
|
||||
pub const CSS_NAVBAR_BRAND: &str = "navbar-brand d-flex align-items-center";
|
||||
pub const CSS_NAVBAR_LINKS_UL: &str = "navbar-nav";
|
||||
|
||||
pub(crate) fn spa_router_user_ui() -> Router<ServerState> {
|
||||
Router::new()
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
|
||||
</head>
|
||||
<body class="flex-column d-flex h-100">
|
||||
<main class="flex-shrink-0 form-signin">
|
||||
<main class="flex-shrink-0 form-signin m-auto">
|
||||
<center>
|
||||
<img
|
||||
src="/pkg/img/logo-square.svg?v={cache_buster_key}"
|
||||
|
|
|
@ -10,12 +10,16 @@ use axum_htmx::HxPushUrl;
|
|||
use kanidm_proto::internal::AppLink;
|
||||
|
||||
use super::constants::Urls;
|
||||
use super::navbar::NavbarCtx;
|
||||
use crate::https::views::errors::HtmxError;
|
||||
use crate::https::{extractors::VerifiedClientInformation, middleware::KOpId, ServerState};
|
||||
use crate::https::{
|
||||
extractors::DomainInfo, extractors::VerifiedClientInformation, middleware::KOpId, ServerState,
|
||||
};
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "apps.html")]
|
||||
struct AppsView {
|
||||
navbar_ctx: NavbarCtx,
|
||||
apps_partial: AppsPartialView,
|
||||
}
|
||||
|
||||
|
@ -29,6 +33,7 @@ pub(crate) async fn view_apps_get(
|
|||
State(state): State<ServerState>,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
|
||||
DomainInfo(domain_info): DomainInfo,
|
||||
) -> axum::response::Result<Response> {
|
||||
// Because this is the route where the login page can land, we need to actually alter
|
||||
// our response as a result. If the user comes here directly we need to render the full
|
||||
|
@ -44,6 +49,7 @@ pub(crate) async fn view_apps_get(
|
|||
(
|
||||
HxPushUrl(Uri::from_static(Urls::Apps.as_ref())),
|
||||
AppsView {
|
||||
navbar_ctx: NavbarCtx { domain_info },
|
||||
apps_partial: AppsPartialView { apps: app_links },
|
||||
},
|
||||
)
|
||||
|
|
|
@ -18,6 +18,7 @@ mod constants;
|
|||
mod cookies;
|
||||
mod errors;
|
||||
mod login;
|
||||
mod navbar;
|
||||
mod oauth2;
|
||||
mod profile;
|
||||
mod reset;
|
||||
|
@ -94,7 +95,10 @@ pub fn view_router() -> Router<ServerState> {
|
|||
.route("/reset/add_password", post(reset::view_new_pwd))
|
||||
.route("/reset/change_password", post(reset::view_new_pwd))
|
||||
.route("/reset/add_passkey", post(reset::view_new_passkey))
|
||||
.route("/reset/set_unixcred", post(reset::view_set_unixcred))
|
||||
.route("/api/delete_alt_creds", post(reset::remove_alt_creds))
|
||||
.route("/api/delete_unixcred", post(reset::remove_unixcred))
|
||||
.route("/api/add_totp", post(reset::add_totp))
|
||||
.route("/api/remove_totp", post(reset::remove_totp))
|
||||
.route("/api/remove_passkey", post(reset::remove_passkey))
|
||||
.route("/api/finish_passkey", post(reset::finish_passkey))
|
||||
|
|
5
server/core/src/https/views/navbar.rs
Normal file
5
server/core/src/https/views/navbar.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
use crate::https::extractors::DomainInfoRead;
|
||||
|
||||
pub struct NavbarCtx {
|
||||
pub domain_info: DomainInfoRead,
|
||||
}
|
|
@ -12,10 +12,12 @@ use kanidm_proto::internal::UserAuthToken;
|
|||
use super::constants::{ProfileMenuItems, UiMessage, Urls};
|
||||
use super::errors::HtmxError;
|
||||
use super::login::{LoginDisplayCtx, Reauth, ReauthPurpose};
|
||||
use super::navbar::NavbarCtx;
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "user_settings.html")]
|
||||
pub(crate) struct ProfileView {
|
||||
navbar_ctx: NavbarCtx,
|
||||
profile_partial: ProfilePartialView,
|
||||
}
|
||||
|
||||
|
@ -27,13 +29,13 @@ struct ProfilePartialView {
|
|||
account_name: String,
|
||||
display_name: String,
|
||||
email: Option<String>,
|
||||
posix_enabled: bool,
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -45,13 +47,13 @@ pub(crate) async fn view_profile_get(
|
|||
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,
|
||||
account_name: uat.name().to_string(),
|
||||
display_name: uat.displayname.clone(),
|
||||
email: uat.mail_primary.clone(),
|
||||
posix_enabled: false,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ use serde::{Deserialize, Serialize};
|
|||
use serde_with::skip_serializing_none;
|
||||
use std::fmt;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::str::FromStr;
|
||||
use uuid::Uuid;
|
||||
|
||||
use kanidm_proto::internal::{
|
||||
|
@ -25,6 +26,7 @@ use kanidm_proto::internal::{
|
|||
};
|
||||
|
||||
use super::constants::Urls;
|
||||
use super::navbar::NavbarCtx;
|
||||
use crate::https::extractors::{DomainInfo, DomainInfoRead, VerifiedClientInformation};
|
||||
use crate::https::middleware::KOpId;
|
||||
use crate::https::views::constants::ProfileMenuItems;
|
||||
|
@ -37,6 +39,7 @@ use super::UnrecoverableErrorView;
|
|||
#[derive(Template)]
|
||||
#[template(path = "user_settings.html")]
|
||||
struct ProfileView {
|
||||
navbar_ctx: NavbarCtx,
|
||||
profile_partial: CredStatusView,
|
||||
}
|
||||
|
||||
|
@ -62,7 +65,6 @@ struct CredStatusView {
|
|||
menu_active_item: ProfileMenuItems,
|
||||
names: String,
|
||||
credentials_update_partial: CredResetPartialView,
|
||||
posix_enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
|
@ -76,6 +78,8 @@ struct CredResetPartialView {
|
|||
attested_passkeys: Vec<PasskeyDetail>,
|
||||
passkeys: Vec<PasskeyDetail>,
|
||||
primary: Option<CredentialDetail>,
|
||||
unixcred_state: CUCredState,
|
||||
unixcred: Option<CredentialDetail>,
|
||||
}
|
||||
|
||||
#[skip_serializing_none]
|
||||
|
@ -91,6 +95,12 @@ struct AddPasswordPartial {
|
|||
check_res: PwdCheckResult,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "credential_update_set_unixcred_partial.html")]
|
||||
struct SetUnixCredPartial {
|
||||
check_res: PwdCheckResult,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
enum PwdCheckResult {
|
||||
Success,
|
||||
|
@ -111,7 +121,7 @@ pub(crate) struct NewPassword {
|
|||
pub(crate) struct NewTotp {
|
||||
name: String,
|
||||
#[serde(rename = "checkTOTPCode")]
|
||||
check_totpcode: u32,
|
||||
check_totpcode: String,
|
||||
#[serde(rename = "ignoreBrokenApp")]
|
||||
ignore_broken_app: bool,
|
||||
}
|
||||
|
@ -154,44 +164,28 @@ pub(crate) struct TOTPRemoveData {
|
|||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub(crate) enum TotpCheckResult {
|
||||
Init {
|
||||
secret: String,
|
||||
qr_code_svg: String,
|
||||
steps: u64,
|
||||
digits: u8,
|
||||
algo: TotpAlgo,
|
||||
uri: String,
|
||||
},
|
||||
Failure {
|
||||
wrong_code: bool,
|
||||
broken_app: bool,
|
||||
warnings: Vec<TotpFeedback>,
|
||||
},
|
||||
pub(crate) struct TotpInit {
|
||||
secret: String,
|
||||
qr_code_svg: String,
|
||||
steps: u64,
|
||||
digits: u8,
|
||||
algo: TotpAlgo,
|
||||
uri: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub(crate) enum TotpFeedback {
|
||||
BlankName,
|
||||
DuplicateName,
|
||||
}
|
||||
|
||||
impl Display for TotpFeedback {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
TotpFeedback::BlankName => write!(f, "Please enter a name."),
|
||||
TotpFeedback::DuplicateName => write!(
|
||||
f,
|
||||
"This name already exists, choose another or remove the existing one."
|
||||
),
|
||||
}
|
||||
}
|
||||
#[derive(Serialize, Deserialize, Debug, Default)]
|
||||
pub(crate) struct TotpCheck {
|
||||
wrong_code: bool,
|
||||
broken_app: bool,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "credential_update_add_totp_partial.html")]
|
||||
struct AddTotpPartial {
|
||||
check_res: TotpCheckResult,
|
||||
totp_init: Option<TotpInit>,
|
||||
totp_name: String,
|
||||
totp_value: String,
|
||||
check: TotpCheck,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug, Serialize, Deserialize)]
|
||||
|
@ -285,6 +279,28 @@ pub(crate) async fn remove_alt_creds(
|
|||
Ok(get_cu_partial_response(cu_status))
|
||||
}
|
||||
|
||||
pub(crate) async fn remove_unixcred(
|
||||
State(state): State<ServerState>,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
HxRequest(_hx_request): HxRequest,
|
||||
VerifiedClientInformation(_client_auth_info): VerifiedClientInformation,
|
||||
jar: CookieJar,
|
||||
) -> axum::response::Result<Response> {
|
||||
let cu_session_token: CUSessionToken = get_cu_session(jar).await?;
|
||||
|
||||
let cu_status = state
|
||||
.qe_r_ref
|
||||
.handle_idmcredentialupdate(
|
||||
cu_session_token,
|
||||
CURequest::UnixPasswordRemove,
|
||||
kopid.eventid,
|
||||
)
|
||||
.map_err(|op_err| HtmxError::new(&kopid, op_err))
|
||||
.await?;
|
||||
|
||||
Ok(get_cu_partial_response(cu_status))
|
||||
}
|
||||
|
||||
pub(crate) async fn remove_totp(
|
||||
State(state): State<ServerState>,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
|
@ -428,63 +444,64 @@ pub(crate) async fn view_new_totp(
|
|||
HxRequest(_hx_request): HxRequest,
|
||||
VerifiedClientInformation(_client_auth_info): VerifiedClientInformation,
|
||||
jar: CookieJar,
|
||||
opt_form: Option<Form<NewTotp>>,
|
||||
) -> axum::response::Result<Response> {
|
||||
let cu_session_token = get_cu_session(jar).await?;
|
||||
let push_url = HxPushUrl(Uri::from_static("/ui/reset/add_totp"));
|
||||
let swapped_handler_trigger =
|
||||
HxResponseTrigger::after_swap([HxEvent::new("addTotpSwapped".to_string())]);
|
||||
|
||||
let new_totp = match opt_form {
|
||||
// Initial response handling, user is entering the form for first time
|
||||
None => {
|
||||
let cu_status = state
|
||||
.qe_r_ref
|
||||
.handle_idmcredentialupdate(
|
||||
cu_session_token,
|
||||
CURequest::TotpGenerate,
|
||||
kopid.eventid,
|
||||
)
|
||||
.await
|
||||
// TODO: better handling for invalid mfaregstate state, can be invalid if certain mfa flows were interrupted
|
||||
// TODO: We should maybe automatically cancel the other MFA reg
|
||||
.map_err(|op_err| HtmxError::new(&kopid, op_err))?;
|
||||
let cu_status = state
|
||||
.qe_r_ref
|
||||
.handle_idmcredentialupdate(cu_session_token, CURequest::TotpGenerate, kopid.eventid)
|
||||
.await
|
||||
// TODO: better handling for invalid mfaregstate state, can be invalid if certain mfa flows were interrupted
|
||||
// TODO: We should maybe automatically cancel the other MFA reg
|
||||
.map_err(|op_err| HtmxError::new(&kopid, op_err))?;
|
||||
|
||||
let partial = if let CURegState::TotpCheck(secret) = cu_status.mfaregstate {
|
||||
let uri = secret.to_uri();
|
||||
let svg = match QrCode::new(uri.as_str()) {
|
||||
Ok(qr) => qr.render::<svg::Color>().build(),
|
||||
Err(qr_err) => {
|
||||
error!("Failed to create TOTP QR code: {qr_err}");
|
||||
"QR Code Generation Failed".to_string()
|
||||
}
|
||||
};
|
||||
let partial = if let CURegState::TotpCheck(secret) = cu_status.mfaregstate {
|
||||
let uri = secret.to_uri();
|
||||
let svg = match QrCode::new(uri.as_str()) {
|
||||
Ok(qr) => qr.render::<svg::Color>().build(),
|
||||
Err(qr_err) => {
|
||||
error!("Failed to create TOTP QR code: {qr_err}");
|
||||
"QR Code Generation Failed".to_string()
|
||||
}
|
||||
};
|
||||
|
||||
AddTotpPartial {
|
||||
check_res: TotpCheckResult::Init {
|
||||
secret: secret.get_secret(),
|
||||
qr_code_svg: svg,
|
||||
steps: secret.step,
|
||||
digits: secret.digits,
|
||||
algo: secret.algo,
|
||||
uri,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
return Err(ErrorResponse::from(HtmxError::new(
|
||||
&kopid,
|
||||
OperationError::CannotStartMFADuringOngoingMFASession,
|
||||
)));
|
||||
};
|
||||
|
||||
return Ok((swapped_handler_trigger, push_url, partial).into_response());
|
||||
AddTotpPartial {
|
||||
totp_init: Some(TotpInit {
|
||||
secret: secret.get_secret(),
|
||||
qr_code_svg: svg,
|
||||
steps: secret.step,
|
||||
digits: secret.digits,
|
||||
algo: secret.algo,
|
||||
uri,
|
||||
}),
|
||||
totp_name: Default::default(),
|
||||
totp_value: Default::default(),
|
||||
check: TotpCheck::default(),
|
||||
}
|
||||
|
||||
// User has submitted a totp code
|
||||
Some(Form(new_totp)) => new_totp,
|
||||
} else {
|
||||
return Err(ErrorResponse::from(HtmxError::new(
|
||||
&kopid,
|
||||
OperationError::CannotStartMFADuringOngoingMFASession,
|
||||
)));
|
||||
};
|
||||
|
||||
let cu_status = if new_totp.ignore_broken_app {
|
||||
Ok((push_url, partial).into_response())
|
||||
}
|
||||
|
||||
pub(crate) async fn add_totp(
|
||||
State(state): State<ServerState>,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
HxRequest(_hx_request): HxRequest,
|
||||
VerifiedClientInformation(_client_auth_info): VerifiedClientInformation,
|
||||
jar: CookieJar,
|
||||
new_totp_form: Form<NewTotp>,
|
||||
) -> axum::response::Result<Response> {
|
||||
let cu_session_token = get_cu_session(jar).await?;
|
||||
|
||||
let check_totpcode = u32::from_str(&new_totp_form.check_totpcode).unwrap_or_default();
|
||||
|
||||
let cu_status = if new_totp_form.ignore_broken_app {
|
||||
// Cope with SHA1 apps because the user has intended to do so, their totp code was already verified
|
||||
state.qe_r_ref.handle_idmcredentialupdate(
|
||||
cu_session_token,
|
||||
|
@ -495,25 +512,22 @@ pub(crate) async fn view_new_totp(
|
|||
// Validate totp code example
|
||||
state.qe_r_ref.handle_idmcredentialupdate(
|
||||
cu_session_token,
|
||||
CURequest::TotpVerify(new_totp.check_totpcode, new_totp.name),
|
||||
CURequest::TotpVerify(check_totpcode, new_totp_form.name.clone()),
|
||||
kopid.eventid,
|
||||
)
|
||||
}
|
||||
.await
|
||||
.map_err(|op_err| HtmxError::new(&kopid, op_err))?;
|
||||
|
||||
let warnings = vec![];
|
||||
let check_res = match &cu_status.mfaregstate {
|
||||
let check = match &cu_status.mfaregstate {
|
||||
CURegState::None => return Ok(get_cu_partial_response(cu_status)),
|
||||
CURegState::TotpTryAgain => TotpCheckResult::Failure {
|
||||
CURegState::TotpTryAgain => TotpCheck {
|
||||
wrong_code: true,
|
||||
broken_app: false,
|
||||
warnings,
|
||||
..Default::default()
|
||||
},
|
||||
CURegState::TotpInvalidSha1 => TotpCheckResult::Failure {
|
||||
wrong_code: false,
|
||||
CURegState::TotpInvalidSha1 => TotpCheck {
|
||||
broken_app: true,
|
||||
warnings,
|
||||
..Default::default()
|
||||
},
|
||||
CURegState::TotpCheck(_)
|
||||
| CURegState::BackupCodes(_)
|
||||
|
@ -526,10 +540,23 @@ pub(crate) async fn view_new_totp(
|
|||
}
|
||||
};
|
||||
|
||||
let check_totpcode = if check.wrong_code {
|
||||
String::default()
|
||||
} else {
|
||||
new_totp_form.check_totpcode.clone()
|
||||
};
|
||||
|
||||
let swapped_handler_trigger =
|
||||
HxResponseTrigger::after_swap([HxEvent::new("addTotpSwapped".to_string())]);
|
||||
|
||||
Ok((
|
||||
swapped_handler_trigger,
|
||||
push_url,
|
||||
AddTotpPartial { check_res },
|
||||
AddTotpPartial {
|
||||
totp_init: None,
|
||||
totp_name: new_totp_form.name.clone(),
|
||||
totp_value: check_totpcode,
|
||||
check,
|
||||
},
|
||||
)
|
||||
.into_response())
|
||||
}
|
||||
|
@ -658,6 +685,66 @@ fn add_cu_cookie(
|
|||
jar.add(token_cookie)
|
||||
}
|
||||
|
||||
pub(crate) async fn view_set_unixcred(
|
||||
State(state): State<ServerState>,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
HxRequest(_hx_request): HxRequest,
|
||||
VerifiedClientInformation(_client_auth_info): VerifiedClientInformation,
|
||||
jar: CookieJar,
|
||||
opt_form: Option<Form<NewPassword>>,
|
||||
) -> axum::response::Result<Response> {
|
||||
let cu_session_token: CUSessionToken = get_cu_session(jar).await?;
|
||||
let swapped_handler_trigger =
|
||||
HxResponseTrigger::after_swap([HxEvent::new("addPasswordSwapped".to_string())]);
|
||||
|
||||
let new_passwords = match opt_form {
|
||||
None => {
|
||||
return Ok((
|
||||
swapped_handler_trigger,
|
||||
SetUnixCredPartial {
|
||||
check_res: PwdCheckResult::Init,
|
||||
},
|
||||
)
|
||||
.into_response());
|
||||
}
|
||||
Some(Form(new_passwords)) => new_passwords,
|
||||
};
|
||||
|
||||
let pwd_equal = new_passwords.new_password == new_passwords.new_password_check;
|
||||
let (warnings, status) = if pwd_equal {
|
||||
let res = state
|
||||
.qe_r_ref
|
||||
.handle_idmcredentialupdate(
|
||||
cu_session_token,
|
||||
CURequest::UnixPassword(new_passwords.new_password),
|
||||
kopid.eventid,
|
||||
)
|
||||
.await;
|
||||
match res {
|
||||
Ok(cu_status) => return Ok(get_cu_partial_response(cu_status)),
|
||||
Err(OperationError::PasswordQuality(password_feedback)) => {
|
||||
(password_feedback, StatusCode::UNPROCESSABLE_ENTITY)
|
||||
}
|
||||
Err(operr) => return Err(ErrorResponse::from(HtmxError::new(&kopid, operr))),
|
||||
}
|
||||
} else {
|
||||
(vec![], StatusCode::UNPROCESSABLE_ENTITY)
|
||||
};
|
||||
|
||||
let check_res = PwdCheckResult::Failure {
|
||||
pwd_equal,
|
||||
warnings,
|
||||
};
|
||||
|
||||
Ok((
|
||||
status,
|
||||
swapped_handler_trigger,
|
||||
HxPushUrl(Uri::from_static("/ui/reset/set_unixcred")),
|
||||
AddPasswordPartial { check_res },
|
||||
)
|
||||
.into_response())
|
||||
}
|
||||
|
||||
pub(crate) async fn view_reset_get(
|
||||
State(state): State<ServerState>,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
|
@ -758,6 +845,8 @@ fn get_cu_partial(cu_status: CUStatus) -> CredResetPartialView {
|
|||
passkeys,
|
||||
primary_state,
|
||||
primary,
|
||||
unixcred_state,
|
||||
unixcred,
|
||||
..
|
||||
} = cu_status;
|
||||
|
||||
|
@ -770,6 +859,8 @@ fn get_cu_partial(cu_status: CUStatus) -> CredResetPartialView {
|
|||
passkeys,
|
||||
primary_state,
|
||||
primary,
|
||||
unixcred_state,
|
||||
unixcred,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -799,16 +890,15 @@ fn get_cu_response(
|
|||
if is_logged_in {
|
||||
let cred_status_view = CredStatusView {
|
||||
menu_active_item: ProfileMenuItems::Credentials,
|
||||
domain_info,
|
||||
domain_info: domain_info.clone(),
|
||||
names,
|
||||
credentials_update_partial,
|
||||
// TODO: fill in posix enabled
|
||||
posix_enabled: false,
|
||||
};
|
||||
|
||||
(
|
||||
HxPushUrl(Uri::from_static(Urls::UpdateCredentials.as_ref())),
|
||||
ProfileView {
|
||||
navbar_ctx: NavbarCtx { domain_info },
|
||||
profile_partial: cred_status_view,
|
||||
},
|
||||
)
|
||||
|
|
1113
server/core/static/img/logo.svg
Normal file
1113
server/core/static/img/logo.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 839 KiB |
|
@ -69,7 +69,7 @@ function onPasskeyCreated(assertion) {
|
|||
document.getElementById("passkey-create-data").value = JSON.stringify(creationData)
|
||||
|
||||
// Make the name input visible and hide the "Begin Passkey Enrollment" button
|
||||
document.getElementById("passkeyNamingSafariBtn").classList.add("d-none")
|
||||
document.getElementById("passkeyNamingSafariPre").classList.add("d-none")
|
||||
document.getElementById("passkeyNamingForm").classList.remove("d-none")
|
||||
document.getElementById("passkeyNamingSubmitBtn").classList.remove("d-none")
|
||||
} catch (e) {
|
||||
|
|
|
@ -1,124 +1,52 @@
|
|||
:root {
|
||||
--totp-width-and-height: 30px;
|
||||
--totp-stroke-width: 60px;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.input-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.form-cred-reset-body {
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
padding: 15px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
#settings-window:has(.form-cred-reset-body) .form-cred-reset-body {
|
||||
#settings-window .form-cred-reset-body {
|
||||
max-width: unset;
|
||||
padding: unset;
|
||||
}
|
||||
|
||||
.form-signin-body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-top: 40px;
|
||||
padding-bottom: 40px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.form-signin {
|
||||
max-width: 680px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.form-signin .checkbox {
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.form-signin .form-floating:focus-within {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.form-signin input[type="email"] {
|
||||
margin-bottom: -1px;
|
||||
border-bottom-right-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
.form-signin input[type="password"] {
|
||||
margin-bottom: 10px;
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
|
||||
/* DASHBOARD */
|
||||
|
||||
.dash-body {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.feather {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
|
||||
/*
|
||||
* Sidebar
|
||||
*/
|
||||
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
/* rtl:raw:
|
||||
right: 0;
|
||||
*/
|
||||
bottom: 0;
|
||||
/* rtl:remove */
|
||||
left: 0;
|
||||
z-index: 100; /* Behind the navbar */
|
||||
padding: 48px 0 0; /* Height of navbar */
|
||||
box-shadow: inset -1px 0 0 rgba(0, 0, 0, 0.1);
|
||||
.side-menu {
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.sidebar {
|
||||
top: 5rem;
|
||||
.side-menu-item {
|
||||
--icon-size: 24px;
|
||||
padding: .4rem .7rem;
|
||||
text-decoration: none;
|
||||
|
||||
&.active {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-sticky {
|
||||
position: relative;
|
||||
top: 0;
|
||||
height: calc(100vh - 48px);
|
||||
padding-top: 0.5rem;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */
|
||||
}
|
||||
&:hover, &.active {
|
||||
background-color: var(--bs-gray-300);
|
||||
}
|
||||
|
||||
.sidebar .nav-link {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.sidebar .nav-link .feather {
|
||||
margin-right: 4px;
|
||||
color: #727272;
|
||||
}
|
||||
|
||||
.sidebar .nav-link.active {
|
||||
color: #2470dc;
|
||||
}
|
||||
|
||||
.sidebar .nav-link:hover .feather,
|
||||
.sidebar .nav-link.active .feather {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.sidebar-heading {
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
.icon-container img {
|
||||
filter: invert(40%);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -130,49 +58,6 @@ body {
|
|||
* Navbar
|
||||
*/
|
||||
|
||||
.navbar-brand {
|
||||
padding-top: 0.75rem;
|
||||
padding-bottom: 0.75rem;
|
||||
padding-left: 1rem;
|
||||
padding-right: 2rem;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.navbar .navbar-toggler {
|
||||
top: 0.25rem;
|
||||
right: 1rem;
|
||||
}
|
||||
|
||||
.navbar-toggler-img {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.navbar .form-control {
|
||||
padding: 0.75rem 1rem;
|
||||
border-width: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.form-control-dark {
|
||||
color: #fff;
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.form-control-dark:focus {
|
||||
border-color: transparent;
|
||||
box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
.vert-center {
|
||||
height: 80%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.kanidm_logo {
|
||||
width: 12em;
|
||||
height: 12em;
|
||||
|
@ -186,11 +71,6 @@ body {
|
|||
margin: auto;
|
||||
}
|
||||
|
||||
:root {
|
||||
--totp-width-and-height: 30px;
|
||||
--totp-stroke-width: 60px;
|
||||
}
|
||||
|
||||
.totp-display-container {
|
||||
padding: 5px 10px;
|
||||
display: flex;
|
||||
|
@ -264,15 +144,25 @@ body {
|
|||
min-height: 150px;
|
||||
}
|
||||
|
||||
#graph-container svg {
|
||||
max-width: 100%;
|
||||
height: fit-content;
|
||||
.btn-tiny {
|
||||
--bs-btn-padding-y: .05rem;
|
||||
--bs-btn-padding-x: .4rem;
|
||||
--bs-btn-font-size: .75rem;
|
||||
}
|
||||
|
||||
#cred-update-commit-bar {
|
||||
display: block;
|
||||
/*
|
||||
position: fixed;
|
||||
bottom: .5rem;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
*/
|
||||
background: white;
|
||||
}
|
||||
|
||||
.icon-container {
|
||||
padding: 2px;
|
||||
width: var(--icon-size);
|
||||
height: var(--icon-size);
|
||||
}
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
<main class="p-3 x-auto">
|
||||
<div class="(( crate::https::ui::CSS_PAGE_HEADER ))">
|
||||
<main class="container-lg">
|
||||
<div>
|
||||
<h2>Applications list</h2>
|
||||
</div>
|
||||
<hr />
|
||||
(% if apps.is_empty() %)
|
||||
<h5>No linked applications available</h5>
|
||||
<p>No linked applications available</p>
|
||||
(% else %)
|
||||
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-3 g-3">
|
||||
(% for app in apps %)
|
||||
|
|
|
@ -5,3 +5,4 @@
|
|||
(% block main %)(% endblock %)
|
||||
(% include "signout_modal.html" %)
|
||||
(% endblock %)
|
||||
|
||||
|
|
|
@ -3,7 +3,12 @@
|
|||
<script id="data">(( challenge|safe ))</script>
|
||||
|
||||
<!-- Safari requires a human input to start passkey creation -->
|
||||
<button id="passkeyNamingSafariBtn" class="btn btn-primary">Begin Passkey Enrolment</button>
|
||||
<div id="passkeyNamingSafariPre">
|
||||
<button id="passkeyNamingSafariBtn" class="btn btn-primary">Begin Passkey Enrolment</button>
|
||||
<div class="d-flex justify-content-end pt-3 g-3" hx-target="#credentialUpdateDynamicSection">
|
||||
<button id="password-cancel" type="button" class="btn btn-danger" hx-post="/ui/api/cancel_mfareg">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="passkeyNamingForm" class="g-2 d-none">
|
||||
<b>Adding a new passkey</b>
|
||||
|
|
|
@ -54,7 +54,7 @@
|
|||
</div>
|
||||
</form>
|
||||
<div class="g-3 d-flex justify-content-end" hx-target="#credentialUpdateDynamicSection">
|
||||
<button id="password-cancel" type="button" class="btn btn-danger me-2" hx-get=(Urls::CredReset) hx-target="body">Cancel</button>
|
||||
<button id="password-cancel" type="button" class="btn btn-danger me-2" hx-get=((Urls::CredReset)) hx-target="body">Cancel</button>
|
||||
<button id="password-submit" type="button" class="btn btn-primary"
|
||||
hx-post="/ui/reset/add_password"
|
||||
hx-include="#newPasswordForm"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<div>
|
||||
<div id="totpInfo">
|
||||
(% if let TotpCheckResult::Init with { secret, qr_code_svg, steps, digits, algo, uri } = check_res %)
|
||||
(% if let Some(TotpInit with { secret, qr_code_svg, steps, digits, algo, uri }) = totp_init %)
|
||||
<div>((qr_code_svg|safe))
|
||||
</div>
|
||||
<code>((uri|safe))</code>
|
||||
|
@ -13,86 +13,67 @@
|
|||
<li>Code size: (( digits )) digits</li>
|
||||
</ul>
|
||||
(% endif %)
|
||||
|
||||
</div>
|
||||
<form class="row g-2 pb-3 needs-validation" id="newTotpForm" novalidate>
|
||||
(% let potentially_invalid_name_class = "" %)
|
||||
(% let potentially_invalid_check_class = "" %)
|
||||
(% let wrong_code = false %)
|
||||
(% let broken_app = false %)
|
||||
(% if let TotpCheckResult::Failure with { wrong_code, broken_app, warnings } = check_res %)
|
||||
(% let wrong_code = wrong_code.clone() %)
|
||||
(% let broken_app = broken_app.clone() %)
|
||||
(% if !warnings.is_empty() %)
|
||||
(% let potentially_invalid_name_class = "is-invalid" %)
|
||||
(% endif %)
|
||||
(% if wrong_code %)
|
||||
(% let potentially_invalid_check_class = "is-invalid" %)
|
||||
(% endif %)
|
||||
(% endif %)
|
||||
|
||||
<label for="new-totp-name" class="form-label">Enter a name for your TOTP</label>
|
||||
<input
|
||||
aria-describedby="totp-name-validation-feedback"
|
||||
class="form-control ((potentially_invalid_name_class))"
|
||||
name="name"
|
||||
id="new-totp-name"
|
||||
required
|
||||
autofocus
|
||||
/>
|
||||
<!-- bootstrap hides the feedback if we remove is-invalid from the input above -->
|
||||
(% if let TotpCheckResult::Failure with { wrong_code, broken_app, warnings } = check_res %)
|
||||
<div id="totp-name-validation-feedback" class="invalid-feedback d-block">
|
||||
<ul>
|
||||
(% for warn in warnings %)
|
||||
<li>(( warn ))</li>
|
||||
(% endfor %)
|
||||
</ul>
|
||||
</div>
|
||||
(% endif %)
|
||||
<div id="newTotpForm">
|
||||
<form class="row g-2 pb-3 needs-validation" novalidate>
|
||||
<label for="new-totp-name" class="form-label">Enter a name for your TOTP</label>
|
||||
<input
|
||||
aria-describedby="totp-name-validation-feedback"
|
||||
class="form-control"
|
||||
name="name"
|
||||
id="new-totp-name"
|
||||
value="(( totp_name ))"
|
||||
required
|
||||
autofocus
|
||||
/>
|
||||
|
||||
<label for="new-totp-check" class="form-label">Enter a TOTP code to confirm it's working</label>
|
||||
<input
|
||||
aria-describedby="new-totp-check-feedback"
|
||||
class="form-control ((potentially_invalid_check_class))"
|
||||
name="checkTOTPCode"
|
||||
id="new-totp-check"
|
||||
type="number"
|
||||
required
|
||||
/>
|
||||
(% if broken_app || wrong_code %)
|
||||
<div id="neq-totp-validation-feedback" class="invalid-feedback">
|
||||
<ul>
|
||||
(% if wrong_code %)
|
||||
<li>Incorrect TOTP code - Please try again</li>
|
||||
(% endif %)
|
||||
(% if broken_app %)
|
||||
<label for="new-totp-check" class="form-label">Enter a TOTP code to confirm it's working</label>
|
||||
<input
|
||||
aria-describedby="new-totp-check-feedback"
|
||||
class="form-control (%- if check.broken_app || check.wrong_code -%)is-invalid(%- endif -%)"
|
||||
name="checkTOTPCode"
|
||||
id="new-totp-check"
|
||||
value="(( totp_value ))"
|
||||
type="number"
|
||||
required
|
||||
/>
|
||||
|
||||
(% if check.broken_app %)
|
||||
<div id="neq-totp-validation-feedback">
|
||||
<ul>
|
||||
<li>Your authenticator appears to be implemented in a way that uses SHA1, rather than SHA256. Are you sure you want to proceed? If you want to try with a new authenticator, enter a new code.</li>
|
||||
(% endif %)
|
||||
</ul>
|
||||
</div>
|
||||
(% endif %)
|
||||
</ul>
|
||||
</div>
|
||||
(% else if check.wrong_code %)
|
||||
<div id="neq-totp-validation-feedback">
|
||||
<ul>
|
||||
<li>Incorrect TOTP code - Please try again</li>
|
||||
</ul>
|
||||
</div>
|
||||
(% endif %)
|
||||
|
||||
</form>
|
||||
<div class="g-3 d-flex justify-content-end" hx-target="#credentialUpdateDynamicSection">
|
||||
<button id="totp-cancel" type="button" class="btn btn-danger me-2" hx-post="/ui/api/cancel_mfareg">Cancel</button>
|
||||
(% if broken_app %)
|
||||
<button id="totp-submit" type="button" class="btn btn-warning"
|
||||
hx-post="/ui/reset/add_totp"
|
||||
hx-target="#newTotpForm"
|
||||
hx-select="#newTotpForm > *"
|
||||
hx-vals='{"ignoreBrokenApp": true}'
|
||||
hx-include="#newTotpForm"
|
||||
>Accept SHA1</button>
|
||||
(% else %)
|
||||
<button id="totp-submit" type="button" class="btn btn-primary"
|
||||
hx-post="/ui/reset/add_totp"
|
||||
hx-target="#newTotpForm"
|
||||
hx-select="#newTotpForm > *"
|
||||
hx-vals='{"ignoreBrokenApp": false}'
|
||||
hx-include="#newTotpForm"
|
||||
>Add</button>
|
||||
(% endif %)
|
||||
</form>
|
||||
<div class="g-3 d-flex justify-content-end" hx-target="#credentialUpdateDynamicSection">
|
||||
<button id="totp-cancel" type="button" class="btn btn-danger me-2" hx-post="/ui/api/cancel_mfareg">Cancel</button>
|
||||
(% if check.broken_app %)
|
||||
<button id="totp-submit" type="button" class="btn btn-warning"
|
||||
hx-post="/ui/api/add_totp"
|
||||
hx-target="#newTotpForm"
|
||||
hx-select="#newTotpForm > *"
|
||||
hx-vals='{"ignoreBrokenApp": true}'
|
||||
hx-include="#newTotpForm"
|
||||
>Accept SHA1</button>
|
||||
(% else %)
|
||||
<button id="totp-submit" type="button" class="btn btn-primary"
|
||||
hx-post="/ui/api/add_totp"
|
||||
hx-target="#newTotpForm"
|
||||
hx-select="#newTotpForm > *"
|
||||
hx-vals='{"ignoreBrokenApp": false}'
|
||||
hx-include="#newTotpForm"
|
||||
>Add</button>
|
||||
(% endif %)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
<div>
|
||||
<form class="row g-2 pb-3 needs-validation" id="newPasswordForm" novalidate>
|
||||
<input hidden type="text" autocomplete="username" />
|
||||
(% let potentially_invalid_input_class = "" %)
|
||||
(% let potentially_invalid_reinput_class = "" %)
|
||||
(% let pwd_equal = true %)
|
||||
|
||||
(% if let PwdCheckResult::Failure with { pwd_equal, warnings } = check_res %)
|
||||
(% let pwd_equal = pwd_equal.clone() %)
|
||||
(% if !warnings.is_empty() %)
|
||||
(% let potentially_invalid_input_class = "is-invalid" %)
|
||||
(% endif %)
|
||||
(% if pwd_equal %)
|
||||
(% let potentially_invalid_reinput_class = "is-invalid" %)
|
||||
(% endif %)
|
||||
(% endif %)
|
||||
|
||||
<label for="new-password" class="form-label">Enter New Password</label>
|
||||
<input
|
||||
aria-describedby="password-validation-feedback"
|
||||
autocomplete="new-password"
|
||||
class="form-control ((potentially_invalid_input_class))"
|
||||
name="new_password"
|
||||
id="new-password"
|
||||
placeholder=""
|
||||
type="password"
|
||||
required
|
||||
autofocus
|
||||
/>
|
||||
<!-- bootstrap hides the feedback if we remove is-invalid from the input above -->
|
||||
(% if let PwdCheckResult::Failure with { pwd_equal, warnings } = check_res %)
|
||||
<div id="password-validation-feedback" class="invalid-feedback d-block">
|
||||
<ul>
|
||||
(% for warn in warnings %)
|
||||
<li>(( warn ))</li>
|
||||
(% endfor %)
|
||||
</ul>
|
||||
</div>
|
||||
(% endif %)
|
||||
|
||||
<label for="new-password-check" class="form-label">Repeat Password</label>
|
||||
<input
|
||||
aria-describedby="neq-password-validation-feedback"
|
||||
autocomplete="new-password"
|
||||
class="form-control ((potentially_invalid_reinput_class))"
|
||||
name="new_password_check"
|
||||
id="new-password-check"
|
||||
placeholder=""
|
||||
type="password"
|
||||
required
|
||||
/>
|
||||
<div id="neq-password-validation-feedback" class="invalid-feedback">
|
||||
<ul><li>Passwords don't match</li></ul>
|
||||
</div>
|
||||
</form>
|
||||
<div class="g-3 d-flex justify-content-end" hx-target="#credentialUpdateDynamicSection">
|
||||
<button id="password-cancel" type="button" class="btn btn-danger me-2" hx-get=((Urls::CredReset)) hx-target="body">Cancel</button>
|
||||
<button id="password-submit" type="button" class="btn btn-primary"
|
||||
hx-post="/ui/reset/set_unixcred"
|
||||
hx-include="#newPasswordForm"
|
||||
>Submit</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -5,14 +5,13 @@
|
|||
(% endblock %)
|
||||
|
||||
(% block body %)
|
||||
<div class="d-flex align-items-start form-cred-reset-body">
|
||||
<main class="w-100">
|
||||
<div class="py-3 text-center">
|
||||
<h3>Updating Credentials</h3>
|
||||
<p>(( names ))</p>
|
||||
<p>(( domain_info.display_name() ))</p>
|
||||
</div>
|
||||
(( credentials_update_partial|safe ))
|
||||
</main>
|
||||
</div>
|
||||
<main class="form-cred-reset-body container my-auto">
|
||||
<div class="text-center">
|
||||
<h3>Updating Credentials</h3>
|
||||
<p><strong>User:</strong> (( names ))</p>
|
||||
<p><strong>Kanidm domain:</strong> (( domain_info.display_name() ))</p>
|
||||
</div>
|
||||
<hr class="my-4" />
|
||||
(( credentials_update_partial|safe ))
|
||||
</main>
|
||||
(% endblock %)
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
(% endblock %)
|
||||
|
||||
(% block body %)
|
||||
<main class="flex-shrink-0 container form-signin" id="cred-reset-form">
|
||||
<main class="flex-shrink-0 container form-signin m-auto" id="cred-reset-form">
|
||||
<center>
|
||||
<img
|
||||
src="/pkg/img/logo-square.svg?v=((crate::https::cache_buster::get_cache_buster_key()))"
|
||||
|
|
|
@ -3,13 +3,11 @@
|
|||
|
||||
|
||||
(% block settings_window %)
|
||||
<div class="d-flex align-items-start form-cred-reset-body">
|
||||
<div class="w-100">
|
||||
<div class="py-3">
|
||||
<p><strong>User:</strong> (( names ))</p>
|
||||
<p><strong>Kanidm domain:</strong> (( domain_info.display_name() ))</p>
|
||||
</div>
|
||||
(( credentials_update_partial|safe ))
|
||||
<div class="form-cred-reset-body">
|
||||
<div>
|
||||
<p><strong>User:</strong> (( names ))</p>
|
||||
<p><strong>Kanidm domain:</strong> (( domain_info.display_name() ))</p>
|
||||
</div>
|
||||
(( credentials_update_partial|safe ))
|
||||
</div>
|
||||
(% endblock %)
|
||||
|
|
|
@ -1,16 +1,20 @@
|
|||
<hr class="my-4" />
|
||||
<h4>Attested Passkeys</h4>
|
||||
<p>Passkeys originating from a signed authenticator.</p>
|
||||
(% for passkey in attested_passkeys %)
|
||||
<div class="row mb-3">
|
||||
<div class="col d-flex align-items-center"><span>(( passkey.tag ))</span></div>
|
||||
<div class="col d-flex justify-content-end">
|
||||
<button type="button" class="btn btn-danger btn-sml" id="(( passkey.tag ))"
|
||||
<ul class="list-group">
|
||||
(% for passkey in attested_passkeys %)
|
||||
<li class="list-group-item">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>(( passkey.tag ))</div>
|
||||
<div>
|
||||
<button type="button" class="btn btn-danger btn-sml" id="(( passkey.tag ))"
|
||||
hx-target="#credentialUpdateDynamicSection"
|
||||
hx-confirm="Are you sure you want to delete attested passkey (( passkey.tag )) ?"
|
||||
hx-post="/ui/api/remove_passkey" hx-vals='{"uuid": "(( passkey.uuid ))"}'>
|
||||
Remove
|
||||
</button>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
(% endfor %)
|
||||
</li>
|
||||
(% endfor %)
|
||||
</ul>
|
|
@ -5,24 +5,23 @@
|
|||
src="/pkg/external/base64.js?v=((crate::https::cache_buster::get_cache_buster_key()))"
|
||||
async></script>
|
||||
|
||||
<div class="row g-3" id="credentialUpdateDynamicSection"
|
||||
<div id="credentialUpdateDynamicSection"
|
||||
hx-on::before-swap="stillSwapFailureResponse(event)">
|
||||
<form class="needs-validation mb-5 pb-5" novalidate>
|
||||
(% match ext_cred_portal %)
|
||||
(% when CUExtPortal::None %)
|
||||
(% when CUExtPortal::Hidden %)
|
||||
<hr class="my-4" />
|
||||
<p>This account is externally managed. Some features may not be
|
||||
available.</p>
|
||||
(% when CUExtPortal::Some(url) %)
|
||||
<hr class="my-4" />
|
||||
(% when CUExtPortal::Some(url) %)
|
||||
<p>This account is externally managed. Some features may not be
|
||||
available.</p>
|
||||
<a href="(( url ))">Visit the external account portal</a>
|
||||
<hr class="my-4" />
|
||||
(% endmatch %)
|
||||
|
||||
(% if warnings.len() > 0 %)
|
||||
<hr class="my-4">
|
||||
(% for warning in warnings %)
|
||||
(% let is_danger = [CURegWarning::WebauthnAttestationUnsatisfiable,
|
||||
CURegWarning::Unsatisfiable].contains(warning) %)
|
||||
|
@ -57,6 +56,7 @@
|
|||
(% endif %)
|
||||
</div>
|
||||
(% endfor %)
|
||||
<hr class="my-4" />
|
||||
(% endif %)
|
||||
|
||||
<!-- Attested Passkeys -->
|
||||
|
@ -81,15 +81,15 @@
|
|||
(% when CUCredState::Modifiable %)
|
||||
(% include "credentials_update_passkeys.html" %)
|
||||
<!-- Here we are modifiable so we can render the button to add passkeys -->
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-primary"
|
||||
<div class="btn-group mt-4">
|
||||
<button type="button" class="btn btn-secondary"
|
||||
hx-post="/ui/reset/add_passkey"
|
||||
hx-vals='{"class": "Any"}'
|
||||
hx-target="#credentialUpdateDynamicSection">
|
||||
Add Passkey
|
||||
</button>
|
||||
<button type="button"
|
||||
class="btn btn-primary dropdown-toggle dropdown-toggle-split"
|
||||
class="btn btn-secondary dropdown-toggle dropdown-toggle-split"
|
||||
data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<span class="visually-hidden">Toggle Dropdown</span>
|
||||
</button>
|
||||
|
@ -112,9 +112,38 @@
|
|||
(% let primary_state = primary_state %)
|
||||
(% include "credentials_update_primary.html" %)
|
||||
|
||||
(% match unixcred_state %)
|
||||
(% when CUCredState::Modifiable %)
|
||||
<hr class="my-4" />
|
||||
<div id="cred-update-commit-bar" class="toast" role="alert"
|
||||
aria-live="assertive" aria-atomic="true">
|
||||
<h4>UNIX Password</h4>
|
||||
<p>This password is used when authenticating to a UNIX-like system</p>
|
||||
<button type="button" class="btn btn-secondary"
|
||||
hx-post="/ui/reset/set_unixcred"
|
||||
hx-target="#credentialUpdateDynamicSection">
|
||||
Set UNIX Password
|
||||
</button>
|
||||
(% match unixcred %)
|
||||
(% when Some(CredentialDetail { uuid, type_: kanidm_proto::internal::CredentialDetailType::Password }) %)
|
||||
<button type="button" class="btn btn-outline-danger"
|
||||
hx-post="/ui/api/delete_unixcred"
|
||||
hx-target="#credentialUpdateDynamicSection">
|
||||
Delete UNIX Password
|
||||
</button>
|
||||
(% when Some(CredentialDetail { uuid, type_: kanidm_proto::internal::CredentialDetailType::GeneratedPassword }) %)
|
||||
(% when Some(CredentialDetail { uuid, type_: kanidm_proto::internal::CredentialDetailType::Passkey(_) }) %)
|
||||
(% when Some(CredentialDetail { uuid, type_: kanidm_proto::internal::CredentialDetailType::PasswordMfa(_totp_set, _security_key_labels, _backup_codes_remaining)}) %)
|
||||
(% when None %)
|
||||
(% endmatch %)
|
||||
<!-- (% if matches!(primary_state, CUCredState::Modifiable) %)
|
||||
|
||||
(% endif %) -->
|
||||
(% when CUCredState::DeleteOnly %)
|
||||
(% when CUCredState::AccessDeny %)
|
||||
(% when CUCredState::PolicyDeny %)
|
||||
(% endmatch %)
|
||||
|
||||
<hr class="my-4" />
|
||||
<div id="cred-update-commit-bar" class="toast">
|
||||
<div class="toast-body">
|
||||
<span class="d-flex align-items-center">
|
||||
<div>
|
||||
|
|
|
@ -8,11 +8,12 @@
|
|||
<a target="_blank" href="https://support.apple.com/guide/iphone/use-passkeys-to-sign-in-to-apps-and-websites-iphf538ea8d0/ios">iOS</a>
|
||||
have built-in support for passkeys.
|
||||
</p>
|
||||
<ul class="list-group">
|
||||
(% for passkey in passkeys %)
|
||||
<div class="row mb-3">
|
||||
<div class="col d-flex align-items-center"><span>(( passkey.tag ))</span></div>
|
||||
<div class="col d-flex justify-content-end">
|
||||
<button type="button" class="btn btn-danger btn-sml" id="(( passkey.tag ))"
|
||||
<div class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<div>(( passkey.tag ))</div>
|
||||
<div>
|
||||
<button type="button" class="btn btn-danger btn-tiny" id="(( passkey.tag ))"
|
||||
hx-target="#credentialUpdateDynamicSection"
|
||||
hx-confirm="Are you sure you want to delete passkey (( passkey.tag )) ?"
|
||||
hx-post="/ui/api/remove_passkey" hx-vals='{"uuid": "(( passkey.uuid ))"}'>
|
||||
|
@ -21,3 +22,4 @@
|
|||
</div>
|
||||
</div>
|
||||
(% endfor %)
|
||||
</ul>
|
|
@ -17,77 +17,108 @@
|
|||
</p>
|
||||
|
||||
(% if matches!(primary_state, CUCredState::Modifiable) %)
|
||||
(% match primary %)
|
||||
(% when Some(CredentialDetail { uuid, type_: kanidm_proto::internal::CredentialDetailType::Password }) %)
|
||||
<h6><b>Password</b></h6>
|
||||
<p>
|
||||
<button type="button" class="btn btn-primary" hx-post="/ui/reset/change_password">
|
||||
Change Password
|
||||
</button>
|
||||
</p>
|
||||
<h6><b>Time-based One Time Password (TOTP)</b></h6>
|
||||
<p>TOTPs are 6 digit codes generated on-demand as a second authentication factor.</p>
|
||||
<p>
|
||||
<button type="button" class="btn btn-primary" hx-post="/ui/reset/add_totp">
|
||||
Add TOTP
|
||||
</button>
|
||||
</p>
|
||||
<br/>
|
||||
<p>
|
||||
<button type="button" class="btn btn-danger" hx-post="/ui/api/delete_alt_creds" hx-confirm="Delete your Password and any associated MFA?\nNote: this will not remove Passkeys.">
|
||||
Delete Alternative Credentials
|
||||
</button>
|
||||
</p>
|
||||
(% when Some(CredentialDetail { uuid, type_: kanidm_proto::internal::CredentialDetailType::PasswordMfa(totp_set, _security_key_labels, _backup_codes_remaining)}) %)
|
||||
<h6><b>Password</b></h6>
|
||||
<p>
|
||||
<button type="button" class="btn btn-primary" hx-post="/ui/reset/change_password">
|
||||
Change Password
|
||||
</button>
|
||||
</p>
|
||||
<h6><b>Time-based One Time Password (TOTP)</b></h6>
|
||||
<p>TOTPs are 6 digit codes generated on-demand as a second authentication factor.</p>
|
||||
(% for totp in totp_set %)
|
||||
<button type="button" class="btn btn-warning mb-2" hx-post="/ui/api/remove_totp" hx-vals='{"name": "(( totp ))"}'>
|
||||
Remove totp (( totp ))
|
||||
</button>
|
||||
(% endfor %)
|
||||
|
||||
<p>
|
||||
<button type="button" class="btn btn-primary" hx-post="/ui/reset/add_totp">
|
||||
Add TOTP
|
||||
</button>
|
||||
</p>
|
||||
<br/>
|
||||
<p>
|
||||
<button type="button" class="btn btn-danger" hx-post="/ui/api/delete_alt_creds" hx-confirm="Delete your Password and any associated MFA?
|
||||
Note: this will not remove Passkeys.">
|
||||
Delete Alternative Credentials
|
||||
</button>
|
||||
</p>
|
||||
(% when Some(CredentialDetail { uuid, type_: kanidm_proto::internal::CredentialDetailType::GeneratedPassword }) %)
|
||||
<h6><b>Password</b></h6>
|
||||
<p>In order to set up alternative authentication methods, you must delete the generated password.</p>
|
||||
<button type="button" class="btn btn-danger" hx-post="/ui/api/delete_alt_creds" >
|
||||
Delete Generated Password
|
||||
</button>
|
||||
(% when Some(CredentialDetail { uuid, type_: kanidm_proto::internal::CredentialDetailType::Passkey(_) }) %)
|
||||
<p>Webauthn Only - Will migrate to passkeys in a future update</p>
|
||||
<button type="button" class="btn btn-danger" hx-post="/ui/api/delete_alt_creds" hx-confirm="Delete your Password and any associated MFA?
|
||||
Note: this will not remove Passkeys.">
|
||||
Delete Alternative Credentials
|
||||
</button>
|
||||
(% when None %)
|
||||
<button type="button" class="btn btn-warning" hx-post="/ui/reset/add_password">
|
||||
Add Password
|
||||
</button>
|
||||
(% endmatch %)
|
||||
<div class="d-flex flex-column row-gap-4">
|
||||
(% match primary %)
|
||||
(% when Some(CredentialDetail { uuid, type_: kanidm_proto::internal::CredentialDetailType::Password }) %)
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>
|
||||
<h6><b>Password</b></h6>
|
||||
</div>
|
||||
<div class="flex-shrink-0 ps-3">
|
||||
<button type="button" class="btn btn-sm btn-secondary" hx-post="/ui/reset/change_password">
|
||||
Change Password
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>
|
||||
<h6><b>Time-based One Time Password (TOTP)</b></h6>
|
||||
<p>TOTPs are 6 digit codes generated on-demand as a second authentication factor.</p>
|
||||
</div>
|
||||
<div class="flex-shrink-0 ps-3">
|
||||
<button type="button" class="btn btn-sm btn-secondary" hx-post="/ui/reset/add_totp">
|
||||
Add TOTP
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button type="button" class="btn btn-outline-danger" hx-post="/ui/api/delete_alt_creds" hx-confirm="Delete your Password and any associated MFA?\nNote: this will not remove Passkeys.">
|
||||
Delete Alternative Credentials
|
||||
</button>
|
||||
</div>
|
||||
(% when Some(CredentialDetail { uuid, type_: kanidm_proto::internal::CredentialDetailType::PasswordMfa(totp_set, _security_key_labels, _backup_codes_remaining)}) %)
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>
|
||||
<h6><b>Password</b></h6>
|
||||
</div>
|
||||
<div class="flex-shrink-0 ps-3">
|
||||
<button type="button" class="btn btn-sm btn-secondary" hx-post="/ui/reset/change_password">
|
||||
Change Password
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>
|
||||
<h6><b>Time-based One Time Password (TOTP)</b></h6>
|
||||
<p>TOTPs are 6 digit codes generated on-demand as a second authentication factor.</p>
|
||||
</div>
|
||||
<div class="flex-shrink-0 ps-3">
|
||||
<button type="button" class="btn btn-sm btn-secondary" hx-post="/ui/reset/add_totp">
|
||||
Add TOTP
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p>Registered authenticators:</p>
|
||||
<ul class="list-group">
|
||||
(% for totp in totp_set %)
|
||||
<li class="list-group-item">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>(( totp ))</div>
|
||||
<button type="button" class="btn btn-tiny btn-danger" hx-post="/ui/api/remove_totp" hx-vals='{"name": "(( totp ))"}'>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
(% endfor %)
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<button type="button" class="btn btn-outline-danger" hx-post="/ui/api/delete_alt_creds" hx-confirm="Delete your Password and any associated MFA?
|
||||
Note: this will not remove Passkeys.">
|
||||
Delete Alternative Credentials
|
||||
</button>
|
||||
</div>
|
||||
(% when Some(CredentialDetail { uuid, type_: kanidm_proto::internal::CredentialDetailType::GeneratedPassword }) %)
|
||||
<div>
|
||||
<h6><b>Password</b></h6>
|
||||
<p>In order to set up alternative authentication methods, you must delete the generated password.</p>
|
||||
<button type="button" class="btn btn-outline-danger" hx-post="/ui/api/delete_alt_creds" >
|
||||
Delete Generated Password
|
||||
</button>
|
||||
</div>
|
||||
(% when Some(CredentialDetail { uuid, type_: kanidm_proto::internal::CredentialDetailType::Passkey(_) }) %)
|
||||
<div>
|
||||
<p>Webauthn Only - Will migrate to passkeys in a future update</p>
|
||||
<button type="button" class="btn btn-outline-danger" hx-post="/ui/api/delete_alt_creds" hx-confirm="Delete your Password and any associated MFA?
|
||||
Note: this will not remove Passkeys.">
|
||||
Delete Alternative Credentials
|
||||
</button>
|
||||
</div>
|
||||
(% when None %)
|
||||
<div>
|
||||
<button type="button" class="btn btn-warning" hx-post="/ui/reset/add_password">
|
||||
Add Password
|
||||
</button>
|
||||
</div>
|
||||
(% endmatch %)
|
||||
</div>
|
||||
(% else if matches!(primary_state, CUCredState::DeleteOnly) %)
|
||||
<p>
|
||||
<div>
|
||||
<button type="button" class="btn btn-warning" hx-post="/ui/api/delete_alt_creds" hx-confirm="Delete your Password and any associated MFA?
|
||||
Note: this will not remove Passkeys.">
|
||||
Delete Legacy Credentials
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
(% endif %)
|
||||
</div>
|
|
@ -21,26 +21,27 @@
|
|||
value="(( username ))"
|
||||
required=true
|
||||
/>
|
||||
|
||||
<input
|
||||
class="input-hidden"
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
autocomplete="current-password"
|
||||
value=""
|
||||
/>
|
||||
|
||||
<input
|
||||
class="input-hidden"
|
||||
id="totp"
|
||||
name="totp"
|
||||
type="number"
|
||||
autocomplete="one-time-code"
|
||||
value=""
|
||||
/>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- BEGIN: to work better with password managers -->
|
||||
<input
|
||||
class="d-none"
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
autocomplete="current-password"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="d-none"
|
||||
id="totp"
|
||||
name="totp"
|
||||
type="number"
|
||||
autocomplete="one-time-code"
|
||||
value=""
|
||||
/>
|
||||
<!-- END -->
|
||||
|
||||
<div class="mb-3 form-check form-switch">
|
||||
<input
|
||||
type="checkbox"
|
||||
|
|
|
@ -6,24 +6,22 @@
|
|||
(% endblock %)
|
||||
|
||||
(% block body %)
|
||||
<main id="main" class="flex-shrink-0 form-signin">
|
||||
<center>
|
||||
(% if display_ctx.domain_info.image().is_some() %)
|
||||
<img src="/ui/images/domain"
|
||||
alt="Kanidm" class="kanidm_logo" />
|
||||
(% else %)
|
||||
<img
|
||||
src="/pkg/img/logo-square.svg?v=((crate::https::cache_buster::get_cache_buster_key()))"
|
||||
alt="Kanidm" class="kanidm_logo" />
|
||||
(% endif %)
|
||||
<h3>Kanidm</h3>
|
||||
(% if let Some(reauth) = display_ctx.reauth %)
|
||||
<h5>Reauthenticating as (( reauth.username )) to access (( reauth.purpose
|
||||
))</h5>
|
||||
(% endif %)
|
||||
</center>
|
||||
|
||||
<div id="login-form-container" class="container">
|
||||
<main id="main" class="form-signin m-auto align-items-center d-flex flex-column">
|
||||
(% if display_ctx.domain_info.image().is_some() %)
|
||||
<img src="/ui/images/domain"
|
||||
alt="Kanidm" class="kanidm_logo" />
|
||||
(% else %)
|
||||
<img
|
||||
src="/pkg/img/logo-square.svg?v=((crate::https::cache_buster::get_cache_buster_key()))"
|
||||
alt="Kanidm" class="kanidm_logo" />
|
||||
(% endif %)
|
||||
<h3>Kanidm</h3>
|
||||
(% if let Some(reauth) = display_ctx.reauth %)
|
||||
<div class="alert alert-info" role="alert">
|
||||
Reauthenticating as (( reauth.username )) to access (( reauth.purpose ))
|
||||
</div>
|
||||
(% endif %)
|
||||
<div>
|
||||
(% block logincontainer %)
|
||||
(% endblock %)
|
||||
</div>
|
||||
|
|
|
@ -12,22 +12,22 @@
|
|||
src="/pkg/pkhtml.js?v=((crate::https::cache_buster::get_cache_buster_key()))"
|
||||
defer></script>
|
||||
|
||||
<div class="identity-verification-container">
|
||||
(% if passkey %)
|
||||
<form id="cred-form" action="/ui/login/passkey" method="POST">
|
||||
<input hidden="hidden" name="cred" id="cred">
|
||||
<div class="justify-content-center">
|
||||
(% if passkey %)
|
||||
<form id="cred-form" action="/ui/login/passkey" method="POST">
|
||||
<input hidden="hidden" name="cred" id="cred">
|
||||
|
||||
<button hx-disable type="button" class="btn btn-dark"
|
||||
id="start-passkey-button">Use Passkey</button>
|
||||
</form>
|
||||
(% else %)
|
||||
<form id="cred-form" action="/ui/login/seckey" method="POST">
|
||||
<input hidden="hidden" name="cred" id="cred">
|
||||
<button hx-disable type="button" class="btn btn-dark"
|
||||
id="start-passkey-button">Use Passkey</button>
|
||||
</form>
|
||||
(% else %)
|
||||
<form id="cred-form" action="/ui/login/seckey" method="POST">
|
||||
<input hidden="hidden" name="cred" id="cred">
|
||||
|
||||
<button type="button" class="btn btn-dark" id="start-seckey-button"
|
||||
>Use Security Key</button>
|
||||
</form>
|
||||
(% endif %)
|
||||
<button type="button" class="btn btn-dark" id="start-seckey-button"
|
||||
>Use Security Key</button>
|
||||
</form>
|
||||
(% endif %)
|
||||
</div>
|
||||
|
||||
(% endblock %)
|
||||
|
|
|
@ -1,29 +1,38 @@
|
|||
<nav class="(( crate::https::ui::CSS_NAVBAR_NAV ))">
|
||||
<div class="container-fluid">
|
||||
<a class="(( crate::https::ui::CSS_NAVBAR_BRAND ))"
|
||||
href="/ui/apps">Kanidm</a>
|
||||
<div class="container-lg">
|
||||
<a class="(( crate::https::ui::CSS_NAVBAR_BRAND ))" href="/ui/apps">
|
||||
(% if navbar_ctx.domain_info.image().is_some() %)
|
||||
<img src="/ui/images/domain"
|
||||
alt="Kanidm" width="auto" height="40" class="navbar-toggler-img" />
|
||||
(% else %)
|
||||
<img
|
||||
src="/pkg/img/logo.svg?v=((crate::https::cache_buster::get_cache_buster_key()))"
|
||||
alt="Kanidm" width="auto" height="40" class="navbar-toggler-img" />
|
||||
(% endif %)
|
||||
<div class="ps-2">Kanidm</div>
|
||||
</a>
|
||||
|
||||
<!-- this shows a button on mobile devices to open the menu-->
|
||||
<button class="navbar-toggler bg-white" type="button"
|
||||
<button class="navbar-toggler" type="button"
|
||||
data-bs-toggle="collapse" data-bs-target="#navbarCollapse"
|
||||
aria-controls="navbarCollapse" aria-expanded="false"
|
||||
aria-label="Toggle navigation">
|
||||
<img
|
||||
src="/pkg/img/logo-square.svg?v=((crate::https::cache_buster::get_cache_buster_key()))"
|
||||
alt="Toggle navigation" class="navbar-toggler-img" />
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarCollapse">
|
||||
<ul class="(( crate::https::ui::CSS_NAVBAR_LINKS_UL ))">
|
||||
<li class="mb-1">
|
||||
<li>
|
||||
<a class="nav-link" href=((Urls::Apps))>
|
||||
<span data-feather="file"></span>Apps</a>
|
||||
</li>
|
||||
<li class="mb-1">
|
||||
<li>
|
||||
<a class="nav-link" href=((Urls::Profile))>
|
||||
<span data-feather="file"></span>Profile</a>
|
||||
</li>
|
||||
<li class="mb-1">
|
||||
</ul>
|
||||
<ul class="(( crate::https::ui::CSS_NAVBAR_LINKS_UL )) ms-md-auto">
|
||||
<li>
|
||||
<a class="nav-link" href="#" data-bs-toggle="modal"
|
||||
data-bs-target="#signoutModal">Sign out</a>
|
||||
</li>
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
(% endblock %)
|
||||
|
||||
(% block body %)
|
||||
<main id="main" class="flex-shrink-0 form-signin">
|
||||
<main id="main" class="flex-shrink-0 form-signin m-auto">
|
||||
<h2 class="h3 mb-3 fw-normal">Consent to Proceed to (( client_name ))</h2>
|
||||
(% if pii_scopes.is_empty() %)
|
||||
<div>
|
||||
|
|
|
@ -1,29 +1,31 @@
|
|||
(% 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?v=((crate::https::cache_buster::get_cache_buster_key()))"
|
||||
alt>(( label ))
|
||||
</a>
|
||||
<li>
|
||||
<a hx-select="main" hx-target="main" hx-swap="outerHTML show:false"
|
||||
href="(( href ))"
|
||||
class="side-menu-item d-flex rounded link-dark(% if menu_active_item == menu_item %) active(% endif %)">
|
||||
<div class="icon-container align-items-center justify-content-center d-flex me-2">
|
||||
<img class="text-body-secondary"
|
||||
src="/pkg/img/icons/(( icon_name )).svg?v=((crate::https::cache_buster::get_cache_buster_key()))"
|
||||
alt>
|
||||
</div>
|
||||
<div>(( label ))</div>
|
||||
</a>
|
||||
</li>
|
||||
(% endmacro %)
|
||||
|
||||
<main id="main" class="container-xxl pb-5">
|
||||
<main id="main" class="container-lg pb-5">
|
||||
<div class="d-flex flex-sm-row flex-column">
|
||||
<div class="list-group side-menu flex-shrink-0">
|
||||
<ul class="side-menu list-unstyled flex-shrink-0 row-gap-1 d-flex flex-column">
|
||||
(% call side_menu_item("Profile", (Urls::Profile),
|
||||
ProfileMenuItems::UserProfile, "person") %)
|
||||
(% if posix_enabled %)
|
||||
(% call side_menu_item("UNIX Password", (Urls::UpdateCredentials),
|
||||
ProfileMenuItems::UnixPassword, "building-lock") %)
|
||||
(% endif %)
|
||||
(% call side_menu_item("Credentials", (Urls::UpdateCredentials),
|
||||
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 ))">
|
||||
</ul>
|
||||
<div id="settings-window" class="flex-grow-1 ps-sm-4 ps-md-5 pt-sm-0 pt-4">
|
||||
<div>
|
||||
<h2>(% block selected_setting_group %)(% endblock %)</h2>
|
||||
</div>
|
||||
<hr />
|
||||
|
||||
(% block settings_window %)
|
||||
(% endblock %)
|
||||
|
|
|
@ -8,23 +8,23 @@ Profile
|
|||
|
||||
<form>
|
||||
<div class="mb-2 row">
|
||||
<label for="profileUserName" class="col-12 col-md-3 col-lg-2 col-form-label">User name</label>
|
||||
<div class="col-12 col-md-6 col-lg-4">
|
||||
<label for="profileUserName" class="col-12 col-md-3 col-xl-2 col-form-label">User name</label>
|
||||
<div class="col-12 col-md-6 col-lg-5">
|
||||
<input type="text" readonly class="form-control-plaintext" id="profileUserName" value="(( account_name ))">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-2 row">
|
||||
<label for="profileDisplayName" class="col-12 col-md-3 col-lg-2 col-form-label">Display name</label>
|
||||
<div class="col-12 col-md-6 col-lg-4">
|
||||
<label for="profileDisplayName" class="col-12 col-md-3 col-xl-2 col-form-label">Display name</label>
|
||||
<div class="col-12 col-md-6 col-lg-5">
|
||||
<input type="text" class="form-control-plaintext" id="profileDisplayName" value="(( display_name ))" disabled>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-2 row">
|
||||
<label for="profileEmail" class="col-12 col-md-3 col-lg-2 col-form-label">Email</label>
|
||||
<div class="col-12 col-md-6 col-lg-4">
|
||||
<input type="email" disabled class="form-control-plaintext" id="profileEmail" value="(( email.clone().unwrap_or("None configured".to_string())))" >
|
||||
<label for="profileEmail" class="col-12 col-md-3 col-xl-2 col-form-label">Email</label>
|
||||
<div class="col-12 col-md-6 col-lg-5">
|
||||
<input type="email" disabled class="form-control-plaintext" id="profileEmail" value="(( email.clone().unwrap_or("None configured".to_string())))">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use crate::entry::EntryInitNew;
|
||||
use crate::prelude::*;
|
||||
use crate::value::CredentialType;
|
||||
|
||||
use kanidm_proto::internal::{Filter, OperationError, UiHint};
|
||||
|
||||
|
@ -326,6 +327,8 @@ lazy_static! {
|
|||
(Attribute::Class, EntryClass::AccountPolicy.to_value()),
|
||||
// Enforce this is a system protected object
|
||||
(Attribute::Class, EntryClass::System.to_value()),
|
||||
// MFA By Default
|
||||
(Attribute::CredentialTypeMinimum, CredentialType::Mfa.into()),
|
||||
],
|
||||
..Default::default()
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue