Skip to content

Commit

Permalink
Merge pull request #80 from robur-coop/account_
Browse files Browse the repository at this point in the history
User account, browser sessions and password update
  • Loading branch information
hannesm authored Oct 29, 2024
2 parents a11e20a + 6228aec commit 8a36724
Show file tree
Hide file tree
Showing 12 changed files with 1,136 additions and 412 deletions.
104 changes: 87 additions & 17 deletions assets/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";
});
}


Expand Down Expand Up @@ -78,7 +78,7 @@ async function saveConfig() {
const pkeyInput = document.getElementById("private-key").value;
const formAlert = document.getElementById("form-alert");
const formButton = document.getElementById('config-button');
const molly_csrf = document.getElementById("molly-csrf").value.trim();
const molly_csrf = document.getElementById("molly-csrf").value;
formButton.classList.add("disabled");
formButton.innerHTML = `Processing <i class="fa-solid fa-spinner animate-spin text-primary-800"></i>`
formButton.disabled = true;
Expand Down Expand Up @@ -147,10 +147,10 @@ function postAlert(bg_color, content) {

async function deployUnikernel() {
const deployButton = document.getElementById("deploy-button");
const name = document.getElementById("unikernel-name").value.trim();
const arguments = document.getElementById("unikernel-arguments").value.trim();
const name = document.getElementById("unikernel-name").value;
const arguments = document.getElementById("unikernel-arguments").value;
const binary = document.getElementById("unikernel-binary").files[0];
const molly_csrf = document.getElementById("molly-csrf").value.trim();
const molly_csrf = document.getElementById("molly-csrf").value;
const formAlert = document.getElementById("form-alert");
if (!name || !binary) {
formAlert.classList.remove("hidden", "text-primary-500");
Expand Down Expand Up @@ -197,7 +197,7 @@ async function deployUnikernel() {

async function restartUnikernel(name) {
try {
const molly_csrf = document.getElementById("molly-csrf").value.trim();
const molly_csrf = document.getElementById("molly-csrf").value;
const response = await fetch(`/unikernel/restart/${name}`, {
method: 'POST',
body: JSON.stringify({ "name": name, "molly_csrf": molly_csrf }),
Expand All @@ -220,7 +220,7 @@ async function restartUnikernel(name) {

async function destroyUnikernel(name) {
try {
const molly_csrf = document.getElementById("molly-csrf").value.trim();
const molly_csrf = document.getElementById("molly-csrf").value;
const response = await fetch(`/unikernel/destroy/${name}`, {
method: 'POST',
body: JSON.stringify({ "name": name, "molly_csrf": molly_csrf }),
Expand Down Expand Up @@ -257,7 +257,7 @@ function buttonLoading(btn, load, text) {

async function toggleUserStatus(uuid, endpoint) {
try {
const molly_csrf = document.getElementById("molly-csrf").value.trim();
const molly_csrf = document.getElementById("molly-csrf").value;
const response = await fetch(endpoint, {
method: 'POST',
body: JSON.stringify({ uuid, molly_csrf }),
Expand Down Expand Up @@ -315,7 +315,7 @@ async function updatePolicy() {
const formAlert = document.getElementById("form-alert");
const user_id = document.getElementById("user_id").innerText;
const policyButton = document.getElementById("set-policy-btn");
const molly_csrf = document.getElementById("molly-csrf").value.trim();
const molly_csrf = document.getElementById("molly-csrf").value;
try {
buttonLoading(policyButton, true, "Processing...")
const response = await fetch("/api/admin/u/policy/update", {
Expand Down Expand Up @@ -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;
const current_password = document.getElementById("current-password").value;
const new_password = document.getElementById("new-password").value;
const confirm_password = document.getElementById("confirm-password").value;
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;
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")
}
}
2 changes: 1 addition & 1 deletion assets/style.css

Large diffs are not rendered by default.

25 changes: 25 additions & 0 deletions dashboard.ml
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,31 @@ let dashboard_layout (user : User_model.user) ~icon
[];
span [ txt "Activity" ];
];
a
~a:
[
a_href "/account";
a_class
[
"hover:bg-gray-200 hover:text-primary-400 \
font-semibold hover:font-bold \
cursor-pointer rounded p-2 w-full flex \
items-center space-x-1";
];
]
[
i
~a:
[
a_class
[
"fa-solid fa-user text-primary-500 \
text-sm";
];
]
[];
span [ txt "My Account" ];
];
hr ~a:[ a_class [ "my-4" ] ] ();
a
~a:
Expand Down
81 changes: 42 additions & 39 deletions middleware.ml
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
type handler = Httpaf.Reqd.t -> unit Lwt.t
type middleware = handler -> handler

let get_csrf now =
User_model.(
generate_cookie ~name:"molly_csrf"
~uuid:(Uuidm.to_string (generate_uuid ()))
~created_at:now ~expires_in:3600)

let has_cookie cookie_name (reqd : Httpaf.Reqd.t) =
let header header_name reqd =
let headers = (Httpaf.Reqd.request reqd).headers in
match Httpaf.Headers.get headers "Cookie" with
Httpaf.Headers.get headers header_name

let user_agent reqd = header "User-Agent" reqd

let generate_csrf_cookie now reqd =
User_model.generate_cookie ~name:User_model.csrf_cookie
~user_agent:(user_agent reqd)
~uuid:(Uuidm.to_string (User_model.generate_uuid ()))
~created_at:now ~expires_in:3600 ()

let cookie cookie_name (reqd : Httpaf.Reqd.t) =
match header "Cookie" reqd with
| Some cookies ->
let cookie_list = String.split_on_char ';' cookies in
List.find_opt
Expand All @@ -28,8 +33,8 @@ let redirect_to_login reqd ?(msg = "") () =
let header_list =
[
( "Set-Cookie",
"molly_session=;Path=/;HttpOnly=true;Expires=2023-10-27T11:00:00.778Z"
);
User_model.session_cookie
^ "=;Path=/;HttpOnly=true;Expires=2023-10-27T11:00:00.778Z" );
("location", "/sign-in");
("Content-Length", string_of_int (String.length msg));
]
Expand All @@ -43,8 +48,8 @@ let redirect_to_register reqd ?(msg = "") () =
let header_list =
[
( "Set-Cookie",
"molly_session=;Path=/;HttpOnly=true;Expires=2023-10-27T11:00:00.778Z"
);
User_model.session_cookie
^ "=;Path=/;HttpOnly=true;Expires=2023-10-27T11:00:00.778Z" );
("location", "/sign-up");
("Content-Length", string_of_int (String.length msg));
]
Expand Down Expand Up @@ -123,21 +128,29 @@ let cookie_value cookie =
| _ -> Error (`Msg "Bad cookie")

let user_from_auth_cookie cookie users =
match cookie_value cookie with
| Ok cookie_value -> (
match User_model.find_user_by_key cookie_value users with
| Some user -> Ok user
| None -> Error (`Msg "User not found"))
| Error (`Msg s) ->
Logs.err (fun m -> m "Error: %s" s);
Error (`Msg s)
match User_model.find_user_by_key cookie users with
| Some user -> Ok user
| None -> Error (`Msg "User not found")

let user_of_cookie users now reqd =
match has_cookie "molly_session" reqd with
let session_cookie_value reqd =
match cookie User_model.session_cookie reqd with
| Some auth_cookie -> (
match cookie_value auth_cookie with
| Ok cookie_value -> Ok cookie_value
| Error (`Msg s) ->
Logs.err (fun m -> m "Error: %s" s);
Error (`Msg s))
| None ->
Logs.err (fun m ->
m "auth-middleware: No molly-session in cookie header.");
Error (`Msg "User not found")

let user_of_cookie users now reqd =
match session_cookie_value reqd with
| Ok auth_cookie -> (
match user_from_auth_cookie auth_cookie users with
| Ok user -> (
match User_model.user_auth_cookie_from_user user with
match User_model.user_session_cookie auth_cookie user with
| Some cookie -> (
match User_model.is_valid_cookie cookie now with
| true -> Ok user
Expand All @@ -157,19 +170,10 @@ let user_of_cookie users now reqd =
m "auth-middleware: Failed to find user with key %s: %s"
auth_cookie s);
Error (`Msg "User not found"))
| None ->
| Error (`Msg err) ->
Logs.err (fun m ->
m "auth-middleware: No molly-session in cookie header.");
Error (`Msg "User not found")

let session_cookie_value reqd =
match has_cookie "molly_session" reqd with
| Some cookie -> (
match cookie_value cookie with
| Ok "" -> Ok None
| Ok x -> Ok (Some x)
| Error _ as e -> e)
| None -> Error (`Msg "no cookie found")
m "auth-middleware: No molly-session in cookie header. %s" err);
Error (`Msg err)

let auth_middleware now users handler reqd =
match user_of_cookie users now reqd with
Expand All @@ -196,11 +200,10 @@ let is_user_admin_middleware api_meth now users handler reqd =
`Unauthorized user 401 api_meth reqd ()
| Error (`Msg msg) -> redirect_to_login ~msg reqd ()

let csrf_match ~input_csrf ~check_csrf =
String.equal (Utils.Json.clean_string input_csrf) check_csrf
let csrf_match ~input_csrf ~check_csrf = String.equal input_csrf check_csrf

let csrf_cookie_verification form_csrf reqd =
match has_cookie "molly_csrf" reqd with
match cookie User_model.csrf_cookie reqd with
| Some cookie -> (
match cookie_value cookie with
| Ok token -> csrf_match ~input_csrf:form_csrf ~check_csrf:token
Expand All @@ -217,7 +220,7 @@ let csrf_verification users now form_csrf handler reqd =
let user_csrf_token =
List.find_opt
(fun (cookie : User_model.cookie) ->
String.equal cookie.name "molly_csrf")
String.equal cookie.name User_model.csrf_cookie)
user.User_model.cookies
in
match user_csrf_token with
Expand Down
8 changes: 6 additions & 2 deletions storage.ml
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ open Utils.Json

type t = { users : User_model.user list; configuration : Configuration.t }

let current_version = 4
let current_version = 5
(* version history:
1 was initial (fields until email_verification_uuid)
2 added active
3 added super_user (but did not serialise it)
4 properly serialised super_user
5 cookie has two new fields last_access and user_agent
*)

let t_to_json t =
Expand All @@ -26,6 +27,7 @@ let t_of_json json =
| Some (`Int v), Some (`List users), Some configuration ->
let* () =
if v = current_version then Ok ()
else if v = 4 then Ok ()
else if v = 3 then Ok ()
else if v = 2 then Ok ()
else if v = 1 then Ok ()
Expand All @@ -42,7 +44,9 @@ let t_of_json json =
let* user =
if v = 1 then User_model.user_v1_of_json js
else if v = 2 || v = 3 then User_model.user_v2_of_json js
else User_model.user_of_json js
else if v = 4 then
User_model.(user_of_json cookie_v1_of_json) js
else User_model.(user_of_json cookie_of_json) js
in
Ok (user :: acc))
(Ok []) users
Expand Down
Loading

0 comments on commit 8a36724

Please sign in to comment.