From e870dd8acbdfa30dcb375a86d25762becfb49daf Mon Sep 17 00:00:00 2001 From: PizieDust Date: Fri, 25 Oct 2024 00:24:50 +0200 Subject: [PATCH] update password and browser sessions --- assets/main.js | 88 ++++++++++-- unikernel.ml | 21 +++ user_account.ml | 345 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 445 insertions(+), 9 deletions(-) create mode 100644 user_account.ml diff --git a/assets/main.js b/assets/main.js index fb7f4b3..4a6cf92 100644 --- a/assets/main.js +++ b/assets/main.js @@ -32,15 +32,15 @@ function getUnikernelName(url) { } function filterData() { - const input = document.getElementById("searchQuery").value.toUpperCase(); - const table = document.getElementById("data-table"); - const rows = Array.from(table.querySelectorAll("tbody tr")); - - rows.forEach(row => { - const cells = Array.from(row.getElementsByTagName("td")); - const match = cells.some(td => td.textContent.toUpperCase().includes(input)); - row.style.display = match ? "" : "none"; - }); + const input = document.getElementById("searchQuery").value.toUpperCase(); + const table = document.getElementById("data-table"); + const rows = Array.from(table.querySelectorAll("tbody tr")); + + rows.forEach(row => { + const cells = Array.from(row.getElementsByTagName("td")); + const match = cells.some(td => td.textContent.toUpperCase().includes(input)); + row.style.display = match ? "" : "none"; + }); } @@ -418,3 +418,73 @@ function sort_data() { }; } +async function updatePassword() { + const passwordButton = document.getElementById("password-button"); + try { + buttonLoading(passwordButton, true, "Updating..") + const molly_csrf = document.getElementById("molly-csrf").value.trim(); + const current_password = document.getElementById("current-password").value.trim(); + const new_password = document.getElementById("new-password").value.trim(); + const confirm_password = document.getElementById("confirm-password").value.trim(); + const formAlert = document.getElementById("form-alert"); + if (!current_password || !new_password || !confirm_password ) { + formAlert.classList.remove("hidden", "text-primary-500"); + formAlert.classList.add("text-secondary-500"); + formAlert.textContent = "Please fill in all the required passwords" + buttonLoading(passwordButton, false, "Deploy") + } else { + const response = await fetch('/account/password/update', { + method: 'POST', + body: JSON.stringify( + { + molly_csrf, + current_password, + new_password, + confirm_password + + }), + headers: { 'Content-Type': 'application/json' } + }); + + const data = await response.json(); + if (response.status === 200) { + postAlert("bg-primary-300", data.data); + setTimeout(() => window.location.reload(), 1000); + } else { + postAlert("bg-secondary-300", data.data); + buttonLoading(passwordButton, false, "Save") + } + } + } catch (error) { + postAlert("bg-secondary-300", error); + buttonLoading(passwordButton, false, "Save") + } +} + +async function closeSessions() { + const sessionButton = document.getElementById("session-button"); + try { + buttonLoading(sessionButton, true, "Closing sessions..") + const molly_csrf = document.getElementById("molly-csrf").value.trim(); + const response = await fetch('/account/sessions/close', { + method: 'POST', + body: JSON.stringify( + { + molly_csrf, + }), + headers: { 'Content-Type': 'application/json' } + }); + + const data = await response.json(); + if (response.status === 200) { + postAlert("bg-primary-300", data.data); + setTimeout(() => window.location.reload(), 1000); + } else { + postAlert("bg-secondary-300", data.data); + buttonLoading(sessionButton, false, "Logout all other sessions") + } + } catch (error) { + postAlert("bg-secondary-300", error); + buttonLoading(sessionButton, false, "Logout all other sessions") + } +} diff --git a/unikernel.ml b/unikernel.ml index 7b18fec..7cba552 100644 --- a/unikernel.ml +++ b/unikernel.ml @@ -1103,6 +1103,27 @@ struct | "/dashboard" -> check_meth `GET (fun () -> authenticate !store reqd (dashboard !albatross reqd)) +| "/account" -> + check_meth `GET (fun () -> + authenticate store reqd (account_page store reqd)) + | "/account/password/update" -> + check_meth `POST (fun () -> + extract_csrf_token reqd >>= function + | Ok (form_csrf, json) -> + authenticate ~form_csrf store reqd + (update_password json store reqd) + | Error (`Msg msg) -> + Middleware.http_response reqd ~title:"Error" + ~data:(String.escaped msg) `Bad_request) + | "/account/sessions/close" -> + check_meth `POST (fun () -> + extract_csrf_token reqd >>= function + | Ok (form_csrf, _) -> + authenticate ~form_csrf store reqd + (close_sessions store reqd) + | Error (`Msg msg) -> + Middleware.http_response reqd ~title:"Error" + ~data:(String.escaped msg) `Bad_request) | "/admin/users" -> check_meth `GET (fun () -> authenticate ~check_admin:true !store reqd (users !store reqd)) diff --git a/user_account.ml b/user_account.ml new file mode 100644 index 0000000..cb51dee --- /dev/null +++ b/user_account.ml @@ -0,0 +1,345 @@ +let user_account_layout ~csrf (user : User_model.user) + (active_cookie : User_model.cookie) current_time = + Tyxml_html.( + section + ~a:[ a_class [ "p-4 bg-gray-50 my-1" ] ] + [ + p + ~a:[ a_class [ "text-3xl font-semibold uppercase" ] ] + [ txt (user.name ^ " - Account") ]; + Utils.csrf_form_input csrf; + section + ~a:[ a_class [ "my-5" ] ] + [ + div + ~a:[ a_class [ "grid grid-cols-2 my-4" ] ] + [ + div + [ + h2 + ~a:[ a_class [ "font-semibold text-xl" ] ] + [ txt "Profile Information" ]; + ]; + div + [ + div + [ + p ~a:[ a_class [ "text-sm my-2" ] ] [ txt "Name" ]; + input + ~a: + [ + a_class + [ + "w-full py-3 px-2 rounded disabled \ + bg-gray-100"; + ]; + a_disabled (); + a_input_type `Text; + a_value user.name; + ] + (); + ]; + div + [ + p + ~a:[ a_class [ "text-sm my-2" ] ] + [ + txt "Email"; + (match user.email_verified with + | Some _ -> + span + ~a:[ a_class [ "text-primary-500" ] ] + [ txt " verified" ] + | None -> + span + ~a:[ a_class [ "text-secondary-500" ] ] + [ txt " not verified" ]); + ]; + input + ~a: + [ + a_class + [ + "w-full py-3 px-2 rounded disabled \ + bg-gray-100"; + ]; + a_disabled (); + a_input_type `Text; + a_value user.email; + ] + (); + ]; + ]; + ]; + hr (); + div + ~a:[ a_class [ "grid grid-cols-2 my-4" ] ] + [ + div + [ + h2 + ~a:[ a_class [ "font-semibold text-xl" ] ] + [ txt "Update Password" ]; + ]; + div + [ + p ~a:[ a_id "form-alert"; a_class [ "my-4" ] ] []; + div + [ + p + ~a:[ a_class [ "text-sm my-2" ] ] + [ txt "Current Password" ]; + input + ~a: + [ + a_input_type `Password; + a_name "current_password"; + a_id "current-password"; + a_class + [ + "ring-primary-100 mt-1.5 transition \ + appearance-none block w-full px-3 py-3 \ + rounded-xl shadow-sm border \ + hover:border-primary-200\n\ + \ \ + focus:border-primary-300 bg-primary-50 \ + bg-opacity-0 hover:bg-opacity-50 \ + focus:bg-opacity-50 ring-primary-200 \ + focus:ring-primary-200\n\ + \ \ + focus:ring-[1px] focus:outline-none"; + ]; + ] + (); + ]; + div + [ + p + ~a:[ a_class [ "text-sm my-2" ] ] + [ txt "New Password" ]; + input + ~a: + [ + a_input_type `Password; + a_name "new_password"; + a_id "new-password"; + a_class + [ + "ring-primary-100 mt-1.5 transition \ + appearance-none block w-full px-3 py-3 \ + rounded-xl shadow-sm border \ + hover:border-primary-200\n\ + \ \ + focus:border-primary-300 bg-primary-50 \ + bg-opacity-0 hover:bg-opacity-50 \ + focus:bg-opacity-50 ring-primary-200 \ + focus:ring-primary-200\n\ + \ \ + focus:ring-[1px] focus:outline-none"; + ]; + ] + (); + ]; + div + [ + p + ~a:[ a_class [ "text-sm my-2" ] ] + [ txt "confirm Password" ]; + input + ~a: + [ + a_input_type `Password; + a_name "confirm_password"; + a_id "confirm-password"; + a_class + [ + "ring-primary-100 mt-1.5 transition \ + appearance-none block w-full px-3 py-3 \ + rounded-xl shadow-sm border \ + hover:border-primary-200\n\ + \ \ + focus:border-primary-300 bg-primary-50 \ + bg-opacity-0 hover:bg-opacity-50 \ + focus:bg-opacity-50 ring-primary-200 \ + focus:ring-primary-200\n\ + \ \ + focus:ring-[1px] focus:outline-none"; + ]; + ] + (); + ]; + div + ~a:[ a_class [ "my-4 w-1/2 text-center" ] ] + [ + button + ~a: + [ + a_id "password-button"; + a_onclick "updatePassword()"; + a_class + [ + "py-3 rounded bg-primary-500 \ + hover:bg-primary-800 w-full text-gray-50 \ + font-semibold"; + ]; + ] + [ txt "Save" ]; + ]; + ]; + ]; + hr (); + div + ~a:[ a_class [ "grid grid-cols-2 my-4" ] ] + [ + div + [ + h2 + ~a:[ a_class [ "font-semibold text-xl" ] ] + [ txt "Browser Sessions" ]; + ]; + div + [ + div + [ + p + ~a:[ a_class [ "my-2" ] ] + [ + txt + "If necessary, you may logout of all of your \ + other browser sessions across all of your \ + devices. Some of your recent sessions are \ + listed below; however, this list may not be \ + exhaustive. If you feel your account has been \ + compromised, you should also update your \ + password."; + ]; + ]; + div + (List.map + (fun (cookie : User_model.cookie) -> + div + ~a:[ a_class [ "flex items-center my-4" ] ] + [ + div + [ + i + ~a: + [ + a_class + [ "fa-solid fa-desktop text-5xl" ]; + ] + []; + ]; + div + ~a:[ a_class [ "ml-3" ] ] + [ + (* TODO: Parse the user-agent string to extract information like OS, browser etc*) + p + ~a:[ a_class [ "text-gray-600" ] ] + [ + txt + (match cookie.user_agent with + | Some agent -> agent + | None -> "User-Agent unavailable"); + ]; + p + ~a:[ a_class [ "text-sm text-gray-600" ] ] + [ + txt "Last active: "; + (if + String.equal cookie.value + active_cookie.value + then + span + [ + span [ txt "now" ]; + span + ~a: + [ + a_class + [ + "text-primary-500 \ + font-semibold"; + ]; + ] + [ txt " This device" ]; + ] + else + match cookie.last_access with + | Some ptime -> + span + [ + txt + (Utils.TimeHelper.time_ago + ~current_time + ~check_time:ptime); + ] + | None -> + span + [ + txt + (Utils.TimeHelper.time_ago + ~current_time + ~check_time: + cookie.created_at); + ]); + ]; + p + ~a:[ a_class [ "text-sm text-gray-600" ] ] + [ + txt "First use: "; + span + [ + txt + (Utils.TimeHelper.time_ago + ~current_time + ~check_time:cookie.created_at); + ]; + ]; + p + ~a:[ a_class [ "text-sm text-gray-600" ] ] + [ + (match + Ptime.add_span cookie.created_at + (Ptime.Span.of_int_s + cookie.expires_in) + with + | Some ptime -> + span + [ + txt + ("Expires on " + ^ Utils.TimeHelper + .string_of_ptime ptime); + ] + | None -> + span + [ txt "Expiry time not available" ]); + ]; + ]; + ]) + (List.filter + (fun (cookie : User_model.cookie) -> + String.equal cookie.name "molly_session") + user.cookies)); + div + ~a:[ a_class [ "my-4 w-1/2 text-center" ] ] + [ + button + ~a: + [ + a_id "session-button"; + a_onclick "closeSessions()"; + a_class + [ + "py-3 rounded bg-secondary-500 \ + hover:bg-secondary-800 w-full text-gray-50 \ + font-semibold"; + ]; + ] + [ txt "Logout all other sessions" ]; + ]; + ]; + ]; + ]; + ])