diff --git a/book/src/developers/designs/profile_display.md b/book/src/developers/designs/profile_display.md new file mode 100644 index 000000000..8550576ba --- /dev/null +++ b/book/src/developers/designs/profile_display.md @@ -0,0 +1,136 @@ +## User Settings Display Web UI + +We need a usable centralized location for users to view and manage their own user settings. +User settings do not envelop credentials, these have their own flow as they are also used in user setup. + + - Writing new values requires a writable session -> we will make the user reauthenticate to obtain a temporary profile-update-token when they want to update their user settings. + - The UI must display and make editable the following categories: + - user attributes + - user ssh public-keys + - The UI must display: + - user credential status as per [credential-display.rst](credential-display.rst) + - user groups + +### User attributes +These consist of: + - username + - displayname + - legal name + - email address +In the future: + - picture + - zoneinfo/timezone + - locale/preferred language + - other business related attributes: address, phone number, ... + +#### Displaying attributes +Attributes should be displayed with + - their descriptive name with tooltips or links if the name may be confusing to non IT people. + - their current value OR if not set: some clear indication that the attribute is not set. + - A method to edit the attribute if it is editable + +#### Editing attributes +Users must be able to edit attributes individually. +Users should be able to see their changes before saving them. + E.g. via a popup that shows the old vs new value asking to confirm. + +#### TODO: Personal Identifiable Information attributes (currently we don't have these attributes) +Certain information should not be displayed in the UI without reauthentication: + - addresses + - phone numbers + - personal emails + - birthdate + +### SSH public keys +Ssh public key entries in kanidm consist of a: + - label : practically the ID of the key in kanidm + - value : the public key + +A user may want to change their laptop ssh key by updating the value while keeping the label the same. +// TODO: Should a user be allowed to relabel their kanidm ssh keys ? + +#### Displaying ssh keys +Due to their long length they should be line-wrapped into a text field so the entirety is visible when shown. +To reduce visible clutter and inconsistent spacing we will put the values into collapsable elements. + +These collapsed elements must include: + - label + - value's key type (ECDSA, rsa, ect..) +and may include: + - value's comment, truncated to some max length + + +#### Editing keys +When editing keys users must be able to add keys, remove keys and update individual key values +Each action will be committed immediately, thus proper prompts and icons indicating this must be shown (like a floppy disk save icon ?) + +### Credential status +Described in [credential-display.rst](credential-display.rst) +Must inform the user of the credential update/reset page, since it is very related and might be what they were looking for instead. + +### User groups +Mostly a technical piece of info, should not be in direct view to avoid confusing users. +Could be displayed in tree form. + +### User profile HTML Structure +To keep things oranised each category will be their own page with a subnavigation bar to navigate between them. +Since htmx cannot (without extensions) swap new scripts into the on swap during boosted navigation, we must do non-boosted navigation to our profile page OR enable some htmx extension library. + +The same htmx limitation means that all JS for every profile categories must be loaded on all profile categories. +Because want to use htmx to swap out content on form submission or page navigation to represent the new state as this is more efficient than triggering the client to do a redirect. + +Every category will get their own Askama template which requires the relevant fields described for each category above. +And example would be +```html + + + + +(% for ssh_key in ssh_keys %) + + (% if ssh_key_is_modifiable %) + + (% endif %) +(% endfor %) + + +(% if ssh_key_is_modifiable %) + +(% endif %) +``` + +```js + + +// Magically gets called on page load and swaps +function onProfileSshKeysSwapped() { + // Do implementation things like attaching event listeners +} + +indow.onload = function () { + // Event triggered by HTMX because we supply a HxTrigger response header when loading this profile category. + document.body.addEventListener("profileSshKeysSwapped", () => { + onProfileSshKeysSwapped() + }); +} +``` + +```rust +#[derive(Template)] +#[template(path = "profile_templates/ssh_keys_partial.html")] +struct SshKeysPartialView { + ssh_keys: Vec, // TODO: Use correct type + modifiable_state: SshKeysModifiabilityThing // ? +} + +fn view_ssh_keys(...) { + // ... + + let ssh_keys_swapped_trigger = HxResponseTrigger::after_swap([HxEvent::new("profileSshKeysSwapped".to_string())]); + Ok(( + ssh_keys_swapped_trigger, + HxPushUrl(Uri::from_static("/ui/profile/ssh_keys")), + HtmlTemplate(SshKeysPartialView { ssh_keys, }) + ).into_response()) +} +``` \ No newline at end of file diff --git a/server/core/src/https/apidocs/tests.rs b/server/core/src/https/apidocs/tests.rs index 082f42d0d..5f9aac8eb 100644 --- a/server/core/src/https/apidocs/tests.rs +++ b/server/core/src/https/apidocs/tests.rs @@ -84,7 +84,7 @@ fn figure_out_if_we_have_all_the_routes() { } // now we check the things for (module, routes) in found_routes { - if ["ui"].contains(&module.as_str()) { + if ["ui", "cookies"].contains(&module.as_str()) { println!( "We can skip checking {} because it's allow-listed for docs", module diff --git a/server/core/src/https/views/apps.rs b/server/core/src/https/views/apps.rs index 4a824234b..2dd87c4d4 100644 --- a/server/core/src/https/views/apps.rs +++ b/server/core/src/https/views/apps.rs @@ -6,7 +6,7 @@ use axum::{ Extension, }; use axum_htmx::extractors::HxRequest; -use axum_htmx::{HxPushUrl, HxReswap, HxRetarget, SwapOption}; +use axum_htmx::HxPushUrl; use kanidm_proto::internal::AppLink; @@ -42,25 +42,18 @@ pub(crate) async fn view_apps_get( .await .map_err(|old| HtmxError::new(&kopid, old))?; - let apps_view = AppsView { - apps_partial: AppsPartialView { apps: app_links }, - }; + let apps_partial = AppsPartialView { apps: app_links }; Ok(if hx_request { ( // On the redirect during a login we don't push urls. We set these headers // so that the url is updated, and we swap the correct element. HxPushUrl(Uri::from_static("/ui/apps")), - // Tell htmx that we want to update the body instead. There is no need - // set the swap value as it defaults to innerHTML. This is because we came here - // from an htmx request so we only need to render the inner portion. - HxRetarget("body".to_string()), - // We send our own main, replace the existing one. - HxReswap(SwapOption::OuterHtml), - HtmlTemplate(apps_view), + HtmlTemplate(apps_partial), ) .into_response() } else { + let apps_view = AppsView { apps_partial }; HtmlTemplate(apps_view).into_response() }) } diff --git a/server/core/src/https/views/cookies.rs b/server/core/src/https/views/cookies.rs new file mode 100644 index 000000000..309191fe7 --- /dev/null +++ b/server/core/src/https/views/cookies.rs @@ -0,0 +1,87 @@ +//! Support Utilities for interacting with cookies. + +use crate::https::ServerState; +use axum_extra::extract::cookie::{Cookie, CookieJar, SameSite}; +use compact_jwt::{Jws, JwsSigner}; +use serde::de::DeserializeOwned; +use serde::Serialize; + +pub fn destroy(jar: CookieJar, ck_id: &str) -> CookieJar { + if let Some(ck) = jar.get(ck_id) { + let mut ck = ck.clone(); + ck.make_removal(); + /* + if let Some(path) = ck.path().cloned() { + ck.set_path(&path); + } + */ + jar.add(ck) + } else { + jar + } +} + +pub fn make_unsigned<'a>( + state: &'_ ServerState, + ck_id: &'a str, + value: String, + path: &'a str, +) -> Cookie<'a> { + let mut token_cookie = Cookie::new(ck_id, value); + token_cookie.set_secure(state.secure_cookies); + token_cookie.set_same_site(SameSite::Lax); + // Prevent Document.cookie accessing this. Still works with fetch. + token_cookie.set_http_only(true); + // We set a domain here because it allows subdomains + // of the idm to share the cookie. If domain was incorrect + // then webauthn won't work anyway! + token_cookie.set_domain(state.domain.clone()); + token_cookie.set_path(path); + token_cookie +} + +pub fn make_signed<'a, T: Serialize>( + state: &'_ ServerState, + ck_id: &'a str, + value: &'_ T, + path: &'a str, +) -> Option> { + let kref = &state.jws_signer; + + let jws = Jws::into_json(value) + .map_err(|e| { + error!(?e); + }) + .ok()?; + + // Get the header token ready. + let token = kref + .sign(&jws) + .map(|jwss| jwss.to_string()) + .map_err(|e| { + error!(?e); + }) + .ok()?; + + let mut token_cookie = Cookie::new(ck_id, token); + token_cookie.set_secure(state.secure_cookies); + token_cookie.set_same_site(SameSite::Lax); + token_cookie.set_http_only(true); + token_cookie.set_path(path); + token_cookie.set_domain(state.domain.clone()); + Some(token_cookie) +} + +pub fn get_signed( + state: &ServerState, + jar: &CookieJar, + ck_id: &str, +) -> Option { + jar.get(ck_id) + .map(|c| c.value()) + .and_then(|s| state.deserialise_from_str::(s)) +} + +pub fn get_unsigned<'a>(jar: &'a CookieJar, ck_id: &'_ str) -> Option<&'a str> { + jar.get(ck_id).map(|c| c.value()) +} diff --git a/server/core/src/https/views/login.rs b/server/core/src/https/views/login.rs index ec520ddf7..555e05207 100644 --- a/server/core/src/https/views/login.rs +++ b/server/core/src/https/views/login.rs @@ -1,3 +1,4 @@ +use super::{cookies, empty_string_as_none, HtmlTemplate, UnrecoverableErrorView}; use crate::https::{extractors::VerifiedClientInformation, middleware::KOpId, ServerState}; use askama::Template; use axum::{ @@ -6,7 +7,6 @@ use axum::{ Extension, Form, Json, }; use axum_extra::extract::cookie::{Cookie, CookieJar, SameSite}; -use compact_jwt::{Jws, JwsSigner}; use kanidm_proto::internal::{ COOKIE_AUTH_SESSION_ID, COOKIE_BEARER_TOKEN, COOKIE_OAUTH2_REQ, COOKIE_USERNAME, }; @@ -21,8 +21,6 @@ use serde::{Deserialize, Serialize}; use std::str::FromStr; use webauthn_rs::prelude::PublicKeyCredential; -use super::{empty_string_as_none, HtmlTemplate, UnrecoverableErrorView}; - #[derive(Default, Serialize, Deserialize)] struct SessionContext { #[serde(rename = "u")] @@ -37,6 +35,9 @@ struct SessionContext { password: Option, #[serde(rename = "t", default, skip_serializing_if = "Option::is_none")] totp: Option, + + #[serde(rename = "a", default, skip_serializing_if = "Option::is_none")] + after_auth_loc: Option, } #[derive(Template)] @@ -116,19 +117,99 @@ pub async fn view_logout_get( } else { let response = Redirect::to("/ui/login").into_response(); - jar = if let Some(bearer_cookie) = jar.get(COOKIE_BEARER_TOKEN) { - let mut bearer_cookie = bearer_cookie.clone(); - bearer_cookie.make_removal(); - bearer_cookie.set_path("/"); - jar.add(bearer_cookie) - } else { - jar - }; + jar = cookies::destroy(jar, COOKIE_BEARER_TOKEN); (jar, response).into_response() } } +pub async fn view_reauth_get( + state: ServerState, + client_auth_info: ClientAuthInfo, + kopid: KOpId, + jar: CookieJar, + return_location: &str, +) -> axum::response::Result { + let session_valid_result = state + .qe_r_ref + .handle_auth_valid(client_auth_info.clone(), kopid.eventid) + .await; + + let res = match session_valid_result { + Ok(()) => { + let inter = state + .qe_r_ref + .handle_reauth( + client_auth_info.clone(), + AuthIssueSession::Cookie, + kopid.eventid, + ) + .await; + + // Now process the response if ok. + match inter { + Ok(ar) => { + let session_context = SessionContext { + id: Some(ar.sessionid), + username: "".to_string(), + password: None, + totp: None, + remember_me: false, + after_auth_loc: Some(return_location.to_string()), + }; + + match view_login_step( + state, + kopid.clone(), + jar, + ar, + client_auth_info, + session_context, + ) + .await + { + Ok(r) => r, + // Okay, these errors are actually REALLY bad. + Err(err_code) => HtmlTemplate(UnrecoverableErrorView { + err_code, + operation_id: kopid.eventid, + }) + .into_response(), + } + } + // Probably needs to be way nicer on login, especially something like no matching users ... + Err(err_code) => HtmlTemplate(UnrecoverableErrorView { + err_code, + operation_id: kopid.eventid, + }) + .into_response(), + } + } + Err(OperationError::NotAuthenticated) | Err(OperationError::SessionExpired) => { + // cookie jar with remember me. + + let username = cookies::get_unsigned(&jar, COOKIE_USERNAME) + .map(String::from) + .unwrap_or_default(); + + let remember_me = !username.is_empty(); + + HtmlTemplate(LoginView { + username, + remember_me, + }) + .into_response() + } + Err(err_code) => HtmlTemplate(UnrecoverableErrorView { + err_code, + operation_id: kopid.eventid, + }) + .into_response(), + }; + + return Ok(res); +} + pub async fn view_index_get( State(state): State, VerifiedClientInformation(client_auth_info): VerifiedClientInformation, @@ -221,6 +302,7 @@ pub async fn view_login_begin_post( password, totp, remember_me, + after_auth_loc: None, }; // Now process the response if ok. @@ -266,14 +348,9 @@ pub async fn view_login_mech_choose_post( jar: CookieJar, Form(login_mech_form): Form, ) -> Response { - let session_context = jar - .get(COOKIE_AUTH_SESSION_ID) - .map(|c| c.value()) - .and_then(|s| { - trace!(id_jws = %s); - state.deserialise_from_str::(s) - }) - .unwrap_or_default(); + let session_context = + cookies::get_signed::(&state, &jar, COOKIE_AUTH_SESSION_ID) + .unwrap_or_default(); debug!("Session ID: {:?}", session_context.id); @@ -411,14 +488,9 @@ async fn credential_step( client_auth_info: ClientAuthInfo, auth_cred: AuthCredential, ) -> Response { - let session_context = jar - .get(COOKIE_AUTH_SESSION_ID) - .map(|c| c.value()) - .and_then(|s| { - trace!(id_jws = %s); - state.deserialise_from_str::(s) - }) - .unwrap_or_default(); + let session_context = + cookies::get_signed::(&state, &jar, COOKIE_AUTH_SESSION_ID) + .unwrap_or_default(); let inter = state // This may change in the future ... .qe_r_ref @@ -477,6 +549,7 @@ async fn view_login_step( state: mut auth_state, sessionid, } = auth_result; + session_context.id = Some(sessionid); let mut safety = 3; @@ -493,28 +566,8 @@ async fn view_login_step( match auth_state { AuthState::Choose(allowed) => { debug!("🧩 -> AuthState::Choose"); - let kref = &state.jws_signer; - // Set the sessionid. - session_context.id = Some(sessionid); - let jws = Jws::into_json(&session_context).map_err(|e| { - error!(?e); - OperationError::InvalidSessionState - })?; - // Get the header token ready. - let token = kref.sign(&jws).map(|jwss| jwss.to_string()).map_err(|e| { - error!(?e); - OperationError::InvalidSessionState - })?; - - let mut token_cookie = Cookie::new(COOKIE_AUTH_SESSION_ID, token); - token_cookie.set_secure(state.secure_cookies); - token_cookie.set_same_site(SameSite::Strict); - token_cookie.set_http_only(true); - // Not setting domains limits the cookie to precisely this - // url that was used. - // token_cookie.set_domain(state.domain.clone()); - jar = jar.add(token_cookie); + jar = add_session_cookie(&state, jar, &session_context)?; let res = match allowed.len() { // Should never happen. @@ -564,6 +617,11 @@ async fn view_login_step( break res; } AuthState::Continue(allowed) => { + // Reauth inits its session here so we need to be able to add cookie here ig. + if jar.get(COOKIE_AUTH_SESSION_ID).is_none() { + jar = add_session_cookie(&state, jar, &session_context)?; + } + let res = match allowed.len() { // Shouldn't be possible. 0 => { @@ -634,25 +692,25 @@ async fn view_login_step( AuthIssueSession::Cookie => { // Update jar let token_str = token.to_string(); - let mut bearer_cookie = Cookie::new(COOKIE_BEARER_TOKEN, token_str.clone()); - bearer_cookie.set_secure(state.secure_cookies); - bearer_cookie.set_same_site(SameSite::Lax); - // Prevent Document.cookie accessing this. Still works with fetch. - bearer_cookie.set_http_only(true); - // We set a domain here because it allows subdomains - // of the idm to share the cookie. If domain was incorrect - // then webauthn won't work anyway! - bearer_cookie.set_domain(state.domain.clone()); - bearer_cookie.set_path("/"); + + // Important - this can be make unsigned as token_str has it's own + // signatures. + let bearer_cookie = cookies::make_unsigned( + &state, + COOKIE_BEARER_TOKEN, + token_str.clone(), + "/", + ); jar = if session_context.remember_me { - let mut username_cookie = - Cookie::new(COOKIE_USERNAME, session_context.username.clone()); - username_cookie.set_secure(state.secure_cookies); - username_cookie.set_same_site(SameSite::Lax); - username_cookie.set_http_only(true); - username_cookie.set_domain(state.domain.clone()); - username_cookie.set_path("/ui/login"); + // Important - can be unsigned as username is just for remember + // me and no other purpose. + let username_cookie = cookies::make_unsigned( + &state, + COOKIE_USERNAME, + session_context.username.clone(), + "/ui/login", + ); jar.add(username_cookie) } else { jar @@ -662,10 +720,11 @@ async fn view_login_step( .add(bearer_cookie) .remove(Cookie::from(COOKIE_AUTH_SESSION_ID)); - // Now, we need to decided where to go. If this - + // Now, we need to decided where to go. let res = if jar.get(COOKIE_OAUTH2_REQ).is_some() { Redirect::to("/ui/oauth2/resume").into_response() + } else if let Some(auth_loc) = session_context.after_auth_loc { + Redirect::to(auth_loc.as_str()).into_response() } else { Redirect::to("/ui/apps").into_response() }; @@ -689,3 +748,17 @@ async fn view_login_step( Ok((jar, response).into_response()) } + +fn add_session_cookie( + state: &ServerState, + jar: CookieJar, + session_context: &SessionContext, +) -> Result { + cookies::make_signed(state, COOKIE_AUTH_SESSION_ID, session_context, "/ui/login") + .map(|mut cookie| { + // Not needed when redirecting into this site + cookie.set_same_site(SameSite::Strict); + jar.add(cookie) + }) + .ok_or(OperationError::InvalidSessionState) +} diff --git a/server/core/src/https/views/mod.rs b/server/core/src/https/views/mod.rs index 73d403859..04f280be2 100644 --- a/server/core/src/https/views/mod.rs +++ b/server/core/src/https/views/mod.rs @@ -17,9 +17,11 @@ use crate::https::{ }; mod apps; +mod cookies; mod errors; mod login; mod oauth2; +mod profile; mod reset; #[derive(Template)] @@ -34,6 +36,8 @@ pub fn view_router() -> Router { .route("/", get(|| async { Redirect::permanent("/ui/login") })) .route("/apps", get(apps::view_apps_get)) .route("/reset", get(reset::view_reset_get)) + .route("/profile", get(profile::view_profile_get)) + .route("/profile/unlock", get(profile::view_profile_unlock_get)) .route("/logout", get(login::view_logout_get)) .route("/oauth2", get(oauth2::view_index_get)) .route("/oauth2/resume", get(oauth2::view_resume_get)) diff --git a/server/core/src/https/views/oauth2.rs b/server/core/src/https/views/oauth2.rs index e131b1d11..f62bf50f1 100644 --- a/server/core/src/https/views/oauth2.rs +++ b/server/core/src/https/views/oauth2.rs @@ -1,4 +1,3 @@ -use compact_jwt::{Jws, JwsSigner}; use kanidmd_lib::idm::oauth2::{ AuthorisationRequest, AuthorisePermitSuccess, AuthoriseResponse, Oauth2Error, }; @@ -18,11 +17,11 @@ use axum::{ response::{IntoResponse, Redirect, Response}, Extension, Form, }; -use axum_extra::extract::cookie::{Cookie, CookieJar, SameSite}; +use axum_extra::extract::cookie::{CookieJar, SameSite}; use axum_htmx::HX_REDIRECT; use serde::Deserialize; -use super::{HtmlTemplate, UnrecoverableErrorView}; +use super::{cookies, HtmlTemplate, UnrecoverableErrorView}; #[derive(Template)] #[template(path = "oauth2_consent_request.html")] @@ -55,10 +54,8 @@ pub async fn view_resume_get( VerifiedClientInformation(client_auth_info): VerifiedClientInformation, jar: CookieJar, ) -> Response { - let maybe_auth_req = jar - .get(COOKIE_OAUTH2_REQ) - .map(|c| c.value()) - .and_then(|s| state.deserialise_from_str::(s)); + let maybe_auth_req = + cookies::get_signed::(&state, &jar, COOKIE_OAUTH2_REQ); if let Some(auth_req) = maybe_auth_req { oauth2_auth_req(state, kopid, client_auth_info, jar, auth_req).await @@ -134,24 +131,16 @@ async fn oauth2_auth_req( .into_response() } Err(Oauth2Error::AuthenticationRequired) => { - // We store the auth_req into the cookie. - let kref = &state.jws_signer; - - let token = Jws::into_json(&auth_req) - .map_err(|err| { - error!(?err, "Failed to serialise AuthorisationRequest"); - OperationError::InvalidSessionState + // Sign the auth req and hide it in our cookie. + let maybe_jar = cookies::make_signed(&state, COOKIE_OAUTH2_REQ, &auth_req, "/ui") + .map(|mut cookie| { + cookie.set_same_site(SameSite::Strict); + jar.add(cookie) }) - .and_then(|jws| { - kref.sign(&jws).map_err(|err| { - error!(?err, "Failed to sign AuthorisationRequest"); - OperationError::InvalidSessionState - }) - }) - .map(|jwss| jwss.to_string()); + .ok_or(OperationError::InvalidSessionState); - let token = match token { - Ok(jws) => jws, + match maybe_jar { + Ok(jar) => (jar, Redirect::to("/ui/login")).into_response(), Err(err_code) => { return HtmlTemplate(UnrecoverableErrorView { err_code, @@ -159,17 +148,7 @@ async fn oauth2_auth_req( }) .into_response(); } - }; - - let mut authreq_cookie = Cookie::new(COOKIE_OAUTH2_REQ, token); - authreq_cookie.set_secure(state.secure_cookies); - authreq_cookie.set_same_site(SameSite::Strict); - authreq_cookie.set_http_only(true); - authreq_cookie.set_domain(state.domain.clone()); - authreq_cookie.set_path("/ui"); - let jar = jar.add(authreq_cookie); - - (jar, Redirect::to("/ui/login")).into_response() + } } Err(Oauth2Error::AccessDenied) => { // If scopes are not available for this account. @@ -227,21 +206,13 @@ pub async fn view_consent_post( state, code, }) => { - let jar = if let Some(authreq_cookie) = jar.get(COOKIE_OAUTH2_REQ) { - let mut authreq_cookie = authreq_cookie.clone(); - authreq_cookie.make_removal(); - authreq_cookie.set_path("/ui"); - jar.add(authreq_cookie) - } else { - jar - }; + let jar = cookies::destroy(jar, COOKIE_OAUTH2_REQ); redirect_uri .query_pairs_mut() .clear() .append_pair("state", &state) .append_pair("code", &code); - ( jar, [ diff --git a/server/core/src/https/views/profile.rs b/server/core/src/https/views/profile.rs new file mode 100644 index 000000000..7e018511e --- /dev/null +++ b/server/core/src/https/views/profile.rs @@ -0,0 +1,80 @@ +use crate::https::extractors::VerifiedClientInformation; +use crate::https::middleware::KOpId; +use crate::https::views::errors::HtmxError; +use crate::https::views::HtmlTemplate; +use crate::https::ServerState; +use askama::Template; +use axum::extract::State; +use axum::http::Uri; +use axum::response::{IntoResponse, Response}; +use axum::Extension; +use axum_extra::extract::cookie::CookieJar; +use axum_htmx::{HxPushUrl, HxRequest}; +use futures_util::TryFutureExt; +use kanidm_proto::internal::UserAuthToken; + +#[derive(Template)] +#[template(path = "user_settings.html")] +struct ProfileView { + profile_partial: ProfilePartialView, +} + +#[derive(Template, Clone)] +#[template(path = "user_settings_profile_partial.html")] +struct ProfilePartialView { + can_rw: bool, + account_name: String, + display_name: String, + legal_name: String, + email: Option, + posix_enabled: bool, +} + +pub(crate) async fn view_profile_get( + State(state): State, + Extension(kopid): Extension, + HxRequest(hx_request): HxRequest, + VerifiedClientInformation(client_auth_info): VerifiedClientInformation, +) -> axum::response::Result { + let uat: UserAuthToken = state + .qe_r_ref + .handle_whoami_uat(client_auth_info, kopid.eventid) + .map_err(|op_err| HtmxError::new(&kopid, op_err)) + .await?; + + let time = time::OffsetDateTime::now_utc() + time::Duration::new(60, 0); + + let can_rw = uat.purpose_readwrite_active(time); + + let profile_partial_view = ProfilePartialView { + can_rw, + account_name: uat.name().to_string(), + display_name: uat.displayname.clone(), + legal_name: uat.name().to_string(), + email: uat.mail_primary.clone(), + posix_enabled: false, + }; + let profile_view = ProfileView { + profile_partial: profile_partial_view.clone(), + }; + + Ok(if hx_request { + ( + HxPushUrl(Uri::from_static("/ui/profile")), + HtmlTemplate(profile_partial_view), + ) + .into_response() + } else { + HtmlTemplate(profile_view).into_response() + }) +} + +// #[axum::debug_handler] +pub(crate) async fn view_profile_unlock_get( + State(state): State, + VerifiedClientInformation(client_auth_info): VerifiedClientInformation, + Extension(kopid): Extension, + jar: CookieJar, +) -> axum::response::Result { + super::login::view_reauth_get(state, client_auth_info, kopid, jar, "/ui/profile").await +} diff --git a/server/core/src/https/views/reset.rs b/server/core/src/https/views/reset.rs index 7cc5def01..2791b72c5 100644 --- a/server/core/src/https/views/reset.rs +++ b/server/core/src/https/views/reset.rs @@ -243,19 +243,6 @@ pub(crate) async fn cancel_mfareg( Ok(get_cu_partial_response(cu_status)) } -async fn get_cu_session(jar: CookieJar) -> Result { - let cookie = jar.get(COOKIE_CU_SESSION_TOKEN); - return if let Some(cookie) = cookie { - let cu_session_token = cookie.value(); - let cu_session_token = CUSessionToken { - token: cu_session_token.into(), - }; - Ok(cu_session_token) - } else { - Err((StatusCode::FORBIDDEN, Redirect::to("/ui/reset")).into_response()) - }; -} - pub(crate) async fn remove_alt_creds( State(state): State, Extension(kopid): Extension, @@ -711,6 +698,19 @@ fn get_cu_response(domain: String, cu_status: CUStatus) -> Response { .into_response() } +async fn get_cu_session(jar: CookieJar) -> Result { + let cookie = jar.get(COOKIE_CU_SESSION_TOKEN); + return if let Some(cookie) = cookie { + let cu_session_token = cookie.value(); + let cu_session_token = CUSessionToken { + token: cu_session_token.into(), + }; + Ok(cu_session_token) + } else { + Err((StatusCode::FORBIDDEN, Redirect::to("/ui/reset")).into_response()) + }; +} + // Any filter defined in the module `filters` is accessible in your template. mod filters { pub fn blank_if( diff --git a/server/core/static/style.css b/server/core/static/style.css index 8949f0299..c2d62c328 100644 --- a/server/core/static/style.css +++ b/server/core/static/style.css @@ -116,6 +116,11 @@ body { text-transform: uppercase; } +/* + * Personal Settings sidemenu + */ + + /* * Navbar */ diff --git a/server/core/templates/navbar.html b/server/core/templates/navbar.html index 47b81cbcb..d330488f2 100644 --- a/server/core/templates/navbar.html +++ b/server/core/templates/navbar.html @@ -10,10 +10,10 @@