From bc977f9e1ca567c4eccb932cf969b39065a324f5 Mon Sep 17 00:00:00 2001 From: PizieDust Date: Fri, 6 Sep 2024 07:07:13 +0200 Subject: [PATCH 01/21] add superuser field --- user_model.ml | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/user_model.ml b/user_model.ml index 04e019e3..d3574829 100644 --- a/user_model.ml +++ b/user_model.ml @@ -28,6 +28,7 @@ type user = { updated_at : Ptime.t; email_verification_uuid : Uuidm.t option; active : bool; + super_user : bool; } let week = 604800 (* a week = 7 days * 24 hours * 60 minutes * 60 seconds *) @@ -256,6 +257,7 @@ let user_v1_of_json = function updated_at = Option.get updated_at; email_verification_uuid; active = true; + super_user = true; } | _ -> Error (`Msg "invalid json for user")) | _ -> Error (`Msg "invalid json for user") @@ -274,7 +276,8 @@ let user_of_json = function get "created_at" xs, get "updated_at" xs, get "email_verification_uuid" xs, - get "active" xs ) + get "active" xs, + get "super_user" xs ) with | ( Some (`String name), Some (`String email), @@ -286,7 +289,8 @@ let user_of_json = function Some (`String updated_at_str), Some (`String created_at_str), Some email_verification_uuid, - Some (`Bool active) ) -> + Some (`Bool active), + Some (`Bool super_user) ) -> let created_at = match Utils.TimeHelper.ptime_of_string created_at_str with | Ok ptime -> Some ptime @@ -339,6 +343,7 @@ let user_of_json = function updated_at = Option.get updated_at; email_verification_uuid; active; + super_user; } | _ -> Error (`Msg "invalid json for user")) | _ -> Error (`Msg "invalid json for user") @@ -391,7 +396,7 @@ let find_user_by_key (uuid : string) (user_map : (string * user) list) : user option = List.assoc_opt uuid user_map -let create_user ~name ~email ~password ~created_at ~active = +let create_user ~name ~email ~password ~created_at ~active ~super_user = let uuid = Uuidm.to_string (generate_uuid ()) in let password = hash_password password uuid in let auth_token = generate_token ~created_at () in @@ -411,13 +416,14 @@ let create_user ~name ~email ~password ~created_at ~active = updated_at = created_at; email_verification_uuid = None; active; + super_user; } let check_if_user_exists email users = List.find_opt (fun user -> user.email = Utils.Json.clean_string email) users let update_user user ?name ?email ?email_verified ?password ?tokens ?cookies - ?updated_at ?email_verification_uuid ?active () = + ?updated_at ?email_verification_uuid ?active ?super_user () = { user with name = Option.value ~default:user.name name; @@ -430,6 +436,7 @@ let update_user user ?name ?email ?email_verified ?password ?tokens ?cookies email_verification_uuid = Option.value ~default:user.email_verification_uuid email_verification_uuid; active = Option.value ~default:user.active active; + super_user = Option.value ~default:user.super_user super_user; } let is_valid_cookie (cookie : cookie) now = From 3596189f87354fbf36e12f15eaf3ce6642b8027e Mon Sep 17 00:00:00 2001 From: PizieDust Date: Fri, 6 Sep 2024 07:07:40 +0200 Subject: [PATCH 02/21] add admin middleware --- middleware.ml | 36 ++++++++++++++++++++++++++++++++---- unikernel.ml | 38 ++++++++++++++++++++++---------------- 2 files changed, 54 insertions(+), 20 deletions(-) diff --git a/middleware.ml b/middleware.ml index 50444767..70868c9b 100644 --- a/middleware.ml +++ b/middleware.ml @@ -46,6 +46,26 @@ let redirect_to_register reqd ?(msg = "") () = Httpaf.Reqd.respond_with_string reqd response msg; Lwt.return_unit +let redirect_to_error ~title ~data user code api_meth reqd () = + let error = { Utils.Status.code; title; success = false; data } in + let data = + if api_meth then Utils.Status.to_json error + else + Dashboard.dashboard_layout user ~page_title:"Unauthorized | Mollymawk" + ~content:(Error_page.error_layout error) + ~icon:"/images/robur.png" () + in + Lwt.return + (let headers = + Httpaf.Headers.of_list + [ + ("content-length", string_of_int (String.length data)); + ("content-type", if api_meth then "application/json" else "text/html"); + ] + in + let resp = Httpaf.Response.create ~headers `Unauthorized in + Httpaf.Reqd.respond_with_string reqd resp data) + let redirect_to_verify_email reqd ?(msg = "") () = let headers = Httpaf.Headers.of_list [ ("location", "/verify-email") ] in let response = Httpaf.Response.create ~headers `Found in @@ -108,10 +128,7 @@ let auth_middleware now users handler reqd = | Ok user -> if user.User_model.active then handler reqd else redirect_to_login ~msg:"User account is deactivated." reqd () - | Error (`Msg msg) -> - Logs.err (fun m -> - m "auth-middleware: No molly-session in cookie header."); - redirect_to_login ~msg reqd () + | Error (`Msg msg) -> redirect_to_login ~msg reqd () let email_verified_middleware now users handler reqd = match user_of_cookie users now reqd with @@ -119,3 +136,14 @@ let email_verified_middleware now users handler reqd = if User_model.is_email_verified user then handler reqd else redirect_to_verify_email reqd () | Error (`Msg msg) -> redirect_to_login ~msg reqd () + +let is_user_admin_middleware api_meth now users handler reqd = + match user_of_cookie users now reqd with + | Ok user -> + if user.User_model.super_user && user.active then handler reqd + else + redirect_to_error ~title:"Unauthorized" + ~data: + "You don't have the necessary permissions to access this service." + user 401 api_meth reqd () + | Error (`Msg msg) -> redirect_to_login ~msg reqd () diff --git a/unikernel.ml b/unikernel.ml index 0b350d84..7e88a791 100644 --- a/unikernel.ml +++ b/unikernel.ml @@ -100,14 +100,18 @@ struct in go (Map.empty, []) m - let authenticate ?(email_verified = true) store reqd f = + let authenticate ?(email_verified = true) ?(check_admin = false) + ?(api_meth = false) store reqd f = let now = Ptime.v (P.now_d_ps ()) in let _, (t : Storage.t) = store in let users = User_model.create_user_session_map t.users in let middlewares = - (if email_verified && false (* TODO *) then - [ Middleware.email_verified_middleware now users ] + (if check_admin then + [ Middleware.is_user_admin_middleware api_meth now users ] else []) + @ (if email_verified && false (* TODO *) then + [ Middleware.email_verified_middleware now users ] + else []) @ [ Middleware.auth_middleware now users ] in Middleware.apply_middleware middlewares @@ -233,9 +237,12 @@ struct | None -> ( let created_at = Ptime.v (P.now_d_ps ()) in let user = - let active = if List.length users = 0 then true else false in + let active, super_user = + if List.length users = 0 then (true, true) + else (false, false) + in User_model.create_user ~name ~email ~password ~created_at - ~active + ~active ~super_user in Store.add_user !store user >>= function | Ok store' -> @@ -478,7 +485,7 @@ struct (Utils.Status.to_json status)) | Error (`Msg s) -> Middleware.redirect_to_login reqd ~msg:s () - let toggle_user store reqd user = + let toggle_user store reqd _user = decode_request_body reqd >>= fun data -> let json = try Ok (Yojson.Basic.from_string data) @@ -1093,24 +1100,23 @@ struct authenticate !store reqd (dashboard !albatross reqd)) | "/admin/users" -> check_meth `GET (fun () -> - (* TODO: a middleware for admins *) - authenticate !store reqd (users !store reqd)) + authenticate ~check_admin:true !store reqd (users !store reqd)) | "/admin/settings" -> check_meth `GET (fun () -> - (* TODO: a middleware for admins *) - authenticate !store reqd (settings !store reqd)) + authenticate ~check_admin:true !store reqd + (settings !store reqd)) | "/api/admin/settings/update" -> check_meth `POST (fun () -> - (* TODO: a middleware for admins *) - authenticate !store reqd + authenticate ~check_admin:true ~api_meth:true !store reqd (update_settings stack store albatross reqd)) | "/api/admin/user/status/toggle" -> check_meth `POST (fun () -> - (* TODO: a middleware for admins *) - authenticate !store reqd (toggle_user store reqd)) - | "/unikernel-info" -> + authenticate ~check_admin:true ~api_meth:true !store reqd + (toggle_user store reqd)) + | "/api/admin/unikernels" -> check_meth `GET (fun () -> - authenticate !store reqd (unikernel_info !albatross reqd)) + authenticate ~check_admin:true ~api_meth:true !store reqd + (unikernel_info !albatross reqd)) | path when String.(length path >= 16 && sub path 0 16 = "/unikernel/info/") -> From 1d73fb49a86f0cb4007111df2d0053f3ad2e6b0c Mon Sep 17 00:00:00 2001 From: PizieDust Date: Fri, 6 Sep 2024 07:08:01 +0200 Subject: [PATCH 03/21] remove admin pages from normal users dashboard --- dashboard.ml | 109 +++++++++++++++++++++++++++------------------------ 1 file changed, 58 insertions(+), 51 deletions(-) diff --git a/dashboard.ml b/dashboard.ml index c120db6b..7a37033e 100644 --- a/dashboard.ml +++ b/dashboard.ml @@ -461,57 +461,64 @@ let dashboard_layout (user : User_model.user) ~icon []; span [ txt "Marketplace" ]; ]; - hr ~a:[ a_class [ "my-4" ] ] (); - a - ~a: - [ - a_href "/admin/users"; - 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-users text-primary-500 \ - text-sm"; - ]; - ] - []; - span [ txt "Users" ]; - ]; - a - ~a: - [ - a_href "/admin/settings"; - 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-gears text-primary-500 \ - text-sm"; - ]; - ] - []; - span [ txt "Settings" ]; - ]; + (if user.super_user then + div + [ + hr ~a:[ a_class [ "my-4" ] ] (); + a + ~a: + [ + a_href "/admin/users"; + 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-users \ + text-primary-500 text-sm"; + ]; + ] + []; + span [ txt "Users" ]; + ]; + a + ~a: + [ + a_href "/admin/settings"; + 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-gears \ + text-primary-500 text-sm"; + ]; + ] + []; + span [ txt "Settings" ]; + ]; + ] + else div []); ]; ]; section From f05cda53b4b9921ce01010ae455fec973b11811d Mon Sep 17 00:00:00 2001 From: PizieDust Date: Fri, 6 Sep 2024 07:08:11 +0200 Subject: [PATCH 04/21] add double checkmark for admins --- users_index.ml | 59 ++++++++++++++++++++++++++++++-------------------- 1 file changed, 36 insertions(+), 23 deletions(-) diff --git a/users_index.ml b/users_index.ml index 33511be5..e17af011 100644 --- a/users_index.ml +++ b/users_index.ml @@ -143,29 +143,42 @@ let users_index_layout (users : User_model.user list) current_time = ] [ p [ txt user.name ]; - (match user.active with - | true -> - i - ~a: - [ - a_class - [ - "text-primary-500 \ - fa-solid fa-check"; - ]; - ] - [] - | false -> - i - ~a: - [ - a_class - [ - "text-secondary-500 \ - fa-solid fa-x"; - ]; - ] - []); + (if user.super_user then + i + ~a: + [ + a_class + [ + "text-primary-800 \ + fa-solid \ + fa-check-double"; + ]; + ] + [] + else + match user.active with + | true -> + i + ~a: + [ + a_class + [ + "text-primary-500 \ + fa-solid fa-check"; + ]; + ] + [] + | false -> + i + ~a: + [ + a_class + [ + "text-secondary-500 \ + fa-solid fa-x"; + ]; + ] + []); ]; ]; td From 8f90754b003e9258feb4d0a0ee21a80899a2ae4a Mon Sep 17 00:00:00 2001 From: PizieDust Date: Fri, 6 Sep 2024 07:14:48 +0200 Subject: [PATCH 05/21] make error redirection dynamic --- middleware.ml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/middleware.ml b/middleware.ml index 70868c9b..9d5a2edc 100644 --- a/middleware.ml +++ b/middleware.ml @@ -46,12 +46,12 @@ let redirect_to_register reqd ?(msg = "") () = Httpaf.Reqd.respond_with_string reqd response msg; Lwt.return_unit -let redirect_to_error ~title ~data user code api_meth reqd () = +let redirect_to_error ~title ~data status user code api_meth reqd () = let error = { Utils.Status.code; title; success = false; data } in let data = if api_meth then Utils.Status.to_json error else - Dashboard.dashboard_layout user ~page_title:"Unauthorized | Mollymawk" + Dashboard.dashboard_layout user ~page_title:(title ^ " | Mollymawk") ~content:(Error_page.error_layout error) ~icon:"/images/robur.png" () in @@ -63,7 +63,7 @@ let redirect_to_error ~title ~data user code api_meth reqd () = ("content-type", if api_meth then "application/json" else "text/html"); ] in - let resp = Httpaf.Response.create ~headers `Unauthorized in + let resp = Httpaf.Response.create ~headers status in Httpaf.Reqd.respond_with_string reqd resp data) let redirect_to_verify_email reqd ?(msg = "") () = @@ -145,5 +145,5 @@ let is_user_admin_middleware api_meth now users handler reqd = redirect_to_error ~title:"Unauthorized" ~data: "You don't have the necessary permissions to access this service." - user 401 api_meth reqd () + `Unauthorized user 401 api_meth reqd () | Error (`Msg msg) -> redirect_to_login ~msg reqd () From 3b701c126dec35dc2215f51bd36ee6cdd9bfd4e2 Mon Sep 17 00:00:00 2001 From: PizieDust Date: Fri, 6 Sep 2024 07:51:59 +0200 Subject: [PATCH 06/21] better http error handling --- unikernel.ml | 148 +++++++++++++++++++++++++-------------------------- 1 file changed, 74 insertions(+), 74 deletions(-) diff --git a/unikernel.ml b/unikernel.ml index 7e88a791..3dbae121 100644 --- a/unikernel.ml +++ b/unikernel.ml @@ -123,7 +123,7 @@ struct assert false) reqd - let reply reqd ?(content_type = "text/plain") ?(header_list = []) data = + let reply reqd ?(content_type = "text/plain") ?(header_list = []) data status = let h = Httpaf.Headers.of_list [ @@ -132,7 +132,7 @@ struct ] in let headers = Httpaf.Headers.add_list h header_list in - let resp = Httpaf.Response.create ~headers `OK in + let resp = Httpaf.Response.create ~headers status in Httpaf.Reqd.respond_with_string reqd resp data let sign_up reqd = @@ -142,12 +142,12 @@ struct | Ok "" -> Lwt.return (reply reqd ~content_type:"text/html" - (Sign_up.register_page ~icon:"/images/robur.png" ())) + (Sign_up.register_page ~icon:"/images/robur.png" ()) `OK) | _ -> Middleware.redirect_to_dashboard reqd ()) | None -> Lwt.return (reply reqd ~content_type:"text/html" - (Sign_up.register_page ~icon:"/images/robur.png" ())) + (Sign_up.register_page ~icon:"/images/robur.png" ()) `OK) let sign_in reqd = match Middleware.has_session_cookie reqd with @@ -156,12 +156,12 @@ struct | Ok "" -> Lwt.return (reply reqd ~content_type:"text/html" - (Sign_in.login_page ~icon:"/images/robur.png" ())) + (Sign_in.login_page ~icon:"/images/robur.png" ()) `OK) | _ -> Middleware.redirect_to_dashboard reqd ()) | None -> Lwt.return (reply reqd ~content_type:"text/html" - (Sign_in.login_page ~icon:"/images/robur.png" ())) + (Sign_in.login_page ~icon:"/images/robur.png" ()) `OK) let register store reqd = decode_request_body reqd >>= fun data -> @@ -182,7 +182,7 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status)) + (Utils.Status.to_json status) `Bad_request) | Ok json -> ( let validate_user_input ~name ~email ~password = if name = "" || email = "" || password = "" then @@ -216,7 +216,7 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status)) + (Utils.Status.to_json status) `Bad_request) | Ok _ -> ( let _, (s : Storage.t) = !store in let users = s.users in @@ -233,7 +233,7 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status)) + (Utils.Status.to_json status) `Bad_request) | None -> ( let created_at = Ptime.v (P.now_d_ps ()) in let user = @@ -272,7 +272,7 @@ struct in Lwt.return (reply reqd ~header_list ~content_type:"application/json" - (Utils.Status.to_json status)) + (Utils.Status.to_json status) `OK) | Error (`Msg err) -> let status = { @@ -284,7 +284,7 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status))))) + (Utils.Status.to_json status) `Bad_request)))) let login store reqd = decode_request_body reqd >>= fun data -> @@ -305,7 +305,7 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status)) + (Utils.Status.to_json status) `Bad_request) | Ok json -> ( let validate_user_input ~email ~password = if email = "" || password = "" then Error "All fields must be filled." @@ -333,7 +333,7 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status)) + (Utils.Status.to_json status) `Bad_request) | Ok _ -> ( let now = Ptime.v (P.now_d_ps ()) in let _, (t : Storage.t) = !store in @@ -351,7 +351,7 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status)) + (Utils.Status.to_json status) `Bad_request) | Ok user -> ( Store.update_user !store user >>= function | Ok store' -> ( @@ -386,7 +386,7 @@ struct Lwt.return (reply reqd ~header_list ~content_type:"application/json" - (Utils.Status.to_json status)) + (Utils.Status.to_json status) `OK) | None -> let status = { @@ -400,7 +400,7 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status))) + (Utils.Status.to_json status) `Bad_request)) | Error (`Msg msg) -> let status = { @@ -412,7 +412,7 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status))))) + (Utils.Status.to_json status) `Bad_request)))) let verify_email store reqd user = let email_verification_uuid = User_model.generate_uuid () in @@ -430,7 +430,7 @@ struct Logs.info (fun m -> m "Verification link is: %s" verification_link); Lwt.return (reply reqd ~content_type:"text/html" - (Verify_email.verify_page ~user ~icon:"/images/robur.png" ())) + (Verify_email.verify_page ~user ~icon:"/images/robur.png" ()) `OK) | Error (`Msg msg) -> let status = { @@ -442,7 +442,7 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status)) + (Utils.Status.to_json status) `Internal_server_error) let verify_email_token store reqd verification_token (user : User_model.user) = @@ -470,7 +470,7 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status)) + (Utils.Status.to_json status) `Internal_server_error) else let status = { @@ -482,7 +482,7 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status)) + (Utils.Status.to_json status) `Bad_request) | Error (`Msg s) -> Middleware.redirect_to_login reqd ~msg:s () let toggle_user store reqd _user = @@ -504,7 +504,7 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status)) + (Utils.Status.to_json status) `Bad_request) | Ok (`Assoc json) -> ( match Utils.Json.get "uuid" json with | Some (`String uuid) -> ( @@ -515,7 +515,7 @@ struct | None -> let status = { - Utils.Status.code = 400; + Utils.Status.code = 404; title = "Error"; data = "Account not found"; success = false; @@ -523,7 +523,7 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status)) + (Utils.Status.to_json status) `Not_found) | Some user -> ( let is_last_active_user = user.active @@ -536,7 +536,7 @@ struct if is_last_active_user then let status = { - Utils.Status.code = 400; + Utils.Status.code = 403; title = "Error"; data = "Refusing to deactivate last active user"; success = false; @@ -544,7 +544,7 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status)) + (Utils.Status.to_json status) `Forbidden) else let user = User_model.update_user user ~active:(not user.active) @@ -564,7 +564,7 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status)) + (Utils.Status.to_json status) `OK) | Error (`Msg msg) -> let status = { @@ -576,12 +576,12 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status)))) + (Utils.Status.to_json status) `Internal_server_error))) | _ -> Logs.warn (fun m -> m "Failed to parse JSON - no UUID found"); let status = { - Utils.Status.code = 400; + Utils.Status.code = 404; title = "Error"; data = "Couldn't find a UUID in the json."; success = false; @@ -589,7 +589,7 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status))) + (Utils.Status.to_json status) `Not_found)) | Ok _ -> let status = { @@ -601,7 +601,7 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status)) + (Utils.Status.to_json status) `Bad_request) let dashboard albatross reqd (user : User_model.user) = (* TODO use uuid in the future *) @@ -626,7 +626,7 @@ struct ~content: (Unikernel_index.unikernel_index_layout unikernels (Ptime.v (P.now_d_ps ()))) - ~icon:"/images/robur.png" ())) + ~icon:"/images/robur.png" ()) `OK) let users store reqd user = Lwt.return @@ -635,7 +635,7 @@ struct ~content: (Users_index.users_index_layout (snd store).Storage.users (Ptime.v (P.now_d_ps ()))) - ~icon:"/images/robur.png" ())) + ~icon:"/images/robur.png" ()) `OK) let settings store reqd user = Lwt.return @@ -643,7 +643,7 @@ struct (Dashboard.dashboard_layout user ~page_title:"Settings | Mollymawk" ~content: (Settings_page.settings_layout (snd store).Storage.configuration) - ~icon:"/images/robur.png" ())) + ~icon:"/images/robur.png" ()) `OK) let update_settings stack store albatross reqd _user = decode_request_body reqd >>= fun data -> @@ -664,7 +664,7 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status)) + (Utils.Status.to_json status) `Bad_request) | Ok json -> ( match Configuration.of_json_from_http json (Ptime.v (P.now_d_ps ())) @@ -690,7 +690,7 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status)) + (Utils.Status.to_json status) `OK) | Error (`Msg err) -> let status = { @@ -702,7 +702,7 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status))) + (Utils.Status.to_json status) `Internal_server_error)) | Error (`Msg err) -> let status = { @@ -714,7 +714,7 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status))) + (Utils.Status.to_json status) `Internal_server_error)) let deploy_form reqd user = Lwt.return @@ -722,7 +722,7 @@ struct (Dashboard.dashboard_layout user ~page_title:"Deploy a Unikernel | Mollymawk" ~content:Unikernel_create.unikernel_create_layout - ~icon:"/images/robur.png" ())) + ~icon:"/images/robur.png" ()) `OK) let unikernel_info albatross reqd (user : User_model.user) = (* TODO use uuid in the future *) @@ -740,7 +740,7 @@ struct } in reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) + (Utils.Status.to_json status) `Internal_server_error | Ok (_hdr, res) -> ( match Albatross_json.res res with | Ok res -> @@ -753,18 +753,18 @@ struct } in reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) + (Utils.Status.to_json status) `OK | Error (`String res) -> let status = { - Utils.Status.code = 400; + Utils.Status.code = 500; title = "Error"; data = Yojson.Safe.to_string (`String res); success = false; } in reply reqd ~content_type:"application/json" - (Utils.Status.to_json status)) + (Utils.Status.to_json status) `Internal_server_error) let unikernel_info_one albatross name reqd (user : User_model.user) = (* TODO use uuid in the future *) @@ -790,11 +790,11 @@ struct ~content: (Unikernel_single.unikernel_single_layout (List.hd unikernels) (Ptime.v (P.now_d_ps ()))) - ~icon:"/images/robur.png" ())) + ~icon:"/images/robur.png" ()) `OK) else let error = { - Utils.Status.code = 400; + Utils.Status.code = 500; title = "An error occured"; success = false; data = "Error while fetching unikernel."; @@ -802,9 +802,9 @@ struct in Lwt.return (reply reqd ~content_type:"text/html" - (Dashboard.dashboard_layout user ~page_title:"404 | Mollymawk" + (Dashboard.dashboard_layout user ~page_title:"An Error Occured | Mollymawk" ~content:(Error_page.error_layout error) - ~icon:"/images/robur.png" ())) + ~icon:"/images/robur.png" ()) `Internal_server_error) let unikernel_destroy albatross name reqd (user : User_model.user) = (* TODO use uuid in the future *) @@ -815,14 +815,14 @@ struct Logs.err (fun m -> m "Error querying albatross: %s" msg); let status = { - Utils.Status.code = 400; + Utils.Status.code = 500; title = "Error"; data = "Error querying albatross: " ^ msg; success = false; } in reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) + (Utils.Status.to_json status) `Internal_server_error | Ok (_hdr, res) -> ( match Albatross_json.res res with | Ok res -> @@ -835,18 +835,18 @@ struct } in reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) + (Utils.Status.to_json status) `OK | Error (`String res) -> let status = { - Utils.Status.code = 400; + Utils.Status.code = 500; title = "Error"; data = Yojson.Safe.to_string (`String res); success = false; } in reply reqd ~content_type:"application/json" - (Utils.Status.to_json status)) + (Utils.Status.to_json status) `Internal_server_error) let unikernel_create albatross reqd (user : User_model.user) = let response_body = Httpaf.Reqd.request_body reqd in @@ -883,7 +883,7 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status)) + (Utils.Status.to_json status) `Bad_request) | Ok ct -> ( match Multipart_form.of_string_to_list data ct with | Error (`Msg msg) -> @@ -898,7 +898,7 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status)) + (Utils.Status.to_json status) `Bad_request) | Ok (m, assoc) -> ( let m, _r = to_map ~assoc m in match @@ -929,7 +929,7 @@ struct } in reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) + (Utils.Status.to_json status) `Internal_server_error | Ok (_hdr, res) -> ( match Albatross_json.res res with | Ok res -> @@ -942,23 +942,23 @@ struct } in reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) + (Utils.Status.to_json status) `OK | Error (`String res) -> let status = { - Utils.Status.code = 400; + Utils.Status.code = 500; title = "Error"; data = Yojson.Safe.to_string (`String res); success = false; } in reply reqd ~content_type:"application/json" - (Utils.Status.to_json status))) + (Utils.Status.to_json status) `Internal_server_error)) | Error (`Msg msg) -> Logs.warn (fun m -> m "couldn't decode data %s" msg); let status = { - Utils.Status.code = 400; + Utils.Status.code = 500; title = "Error"; data = msg; success = false; @@ -966,7 +966,7 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status))) + (Utils.Status.to_json status) `Internal_server_error)) | _ -> Logs.warn (fun m -> m "couldn't find fields"); let status = @@ -979,7 +979,7 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status)))) + (Utils.Status.to_json status) `Bad_request))) let unikernel_console albatross name reqd (user : User_model.user) = (* TODO use uuid in the future *) @@ -998,7 +998,7 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status)) + (Utils.Status.to_json status) `Internal_server_error) | Ok _ -> ( match Result.bind (Vmm_core.Name.path_of_string user.name) (fun domain -> @@ -1011,7 +1011,7 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Yojson.Basic.to_string (`List data))) + (Yojson.Basic.to_string (`List data)) `OK) | Error (`Msg msg) -> let status = { @@ -1023,7 +1023,7 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status))) + (Utils.Status.to_json status) `Internal_server_error)) let request_handler stack albatross js_file css_file imgs store (_ipaddr, _port) reqd = @@ -1039,7 +1039,7 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status)) + (Utils.Status.to_json status) `Bad_request) in let req = Httpaf.Reqd.request reqd in let path = @@ -1054,32 +1054,32 @@ struct (Guest_layout.guest_layout ~page_title:"Deploy unikernels with ease | Mollymawk" ~content:Index_page.index_page ~icon:"/images/robur.png" - ()))) + ()) `OK)) | "/main.js" -> check_meth `GET (fun () -> - Lwt.return (reply reqd ~content_type:"text/plain" js_file)) + Lwt.return (reply reqd ~content_type:"text/plain" js_file `OK)) | "/images/molly_bird.jpeg" -> check_meth `GET (fun () -> Lwt.return - (reply reqd ~content_type:"image/jpeg" imgs.molly_img)) + (reply reqd ~content_type:"image/jpeg" imgs.molly_img `OK)) | "/images/albatross_1.png" -> check_meth `GET (fun () -> Lwt.return - (reply reqd ~content_type:"image/png" imgs.albatross_img)) + (reply reqd ~content_type:"image/png" imgs.albatross_img `OK)) | "/images/dashboard_1.png" -> check_meth `GET (fun () -> Lwt.return - (reply reqd ~content_type:"image/png" imgs.dashboard_img)) + (reply reqd ~content_type:"image/png" imgs.dashboard_img `OK)) | "/images/mirage_os_1.png" -> check_meth `GET (fun () -> Lwt.return - (reply reqd ~content_type:"image/png" imgs.mirage_img)) + (reply reqd ~content_type:"image/png" imgs.mirage_img `OK)) | "/images/robur.png" -> check_meth `GET (fun () -> - Lwt.return (reply reqd ~content_type:"image/png" imgs.robur_img)) + Lwt.return (reply reqd ~content_type:"image/png" imgs.robur_img `OK)) | "/style.css" -> check_meth `GET (fun () -> - Lwt.return (reply reqd ~content_type:"text/css" css_file)) + Lwt.return (reply reqd ~content_type:"text/css" css_file `OK)) | "/sign-up" -> check_meth `GET (fun () -> sign_up reqd) | "/sign-in" -> check_meth `GET (fun () -> sign_in reqd) | "/api/register" -> check_meth `POST (fun () -> register store reqd) @@ -1163,7 +1163,7 @@ struct (reply reqd ~content_type:"text/html" (Guest_layout.guest_layout ~page_title:"404 | Mollymawk" ~content:(Error_page.error_layout error) - ~icon:"/images/robur.png" ()))) + ~icon:"/images/robur.png" ()) `Not_found)) let pp_error ppf = function | #Httpaf.Status.t as code -> Httpaf.Status.pp_hum ppf code From a041c6c8ba313d9253a75f8fd34a9871b88bdd4d Mon Sep 17 00:00:00 2001 From: Auto-OCamlformat Date: Fri, 6 Sep 2024 06:00:20 +0000 Subject: [PATCH 07/21] formatted code --- unikernel.ml | 174 ++++++++++++++++++++++++++++++++++----------------- 1 file changed, 116 insertions(+), 58 deletions(-) diff --git a/unikernel.ml b/unikernel.ml index 3dbae121..af232a31 100644 --- a/unikernel.ml +++ b/unikernel.ml @@ -123,7 +123,8 @@ struct assert false) reqd - let reply reqd ?(content_type = "text/plain") ?(header_list = []) data status = + let reply reqd ?(content_type = "text/plain") ?(header_list = []) data status + = let h = Httpaf.Headers.of_list [ @@ -142,12 +143,14 @@ struct | Ok "" -> Lwt.return (reply reqd ~content_type:"text/html" - (Sign_up.register_page ~icon:"/images/robur.png" ()) `OK) + (Sign_up.register_page ~icon:"/images/robur.png" ()) + `OK) | _ -> Middleware.redirect_to_dashboard reqd ()) | None -> Lwt.return (reply reqd ~content_type:"text/html" - (Sign_up.register_page ~icon:"/images/robur.png" ()) `OK) + (Sign_up.register_page ~icon:"/images/robur.png" ()) + `OK) let sign_in reqd = match Middleware.has_session_cookie reqd with @@ -156,12 +159,14 @@ struct | Ok "" -> Lwt.return (reply reqd ~content_type:"text/html" - (Sign_in.login_page ~icon:"/images/robur.png" ()) `OK) + (Sign_in.login_page ~icon:"/images/robur.png" ()) + `OK) | _ -> Middleware.redirect_to_dashboard reqd ()) | None -> Lwt.return (reply reqd ~content_type:"text/html" - (Sign_in.login_page ~icon:"/images/robur.png" ()) `OK) + (Sign_in.login_page ~icon:"/images/robur.png" ()) + `OK) let register store reqd = decode_request_body reqd >>= fun data -> @@ -182,7 +187,8 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `Bad_request) + (Utils.Status.to_json status) + `Bad_request) | Ok json -> ( let validate_user_input ~name ~email ~password = if name = "" || email = "" || password = "" then @@ -216,7 +222,8 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `Bad_request) + (Utils.Status.to_json status) + `Bad_request) | Ok _ -> ( let _, (s : Storage.t) = !store in let users = s.users in @@ -233,7 +240,8 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `Bad_request) + (Utils.Status.to_json status) + `Bad_request) | None -> ( let created_at = Ptime.v (P.now_d_ps ()) in let user = @@ -272,7 +280,8 @@ struct in Lwt.return (reply reqd ~header_list ~content_type:"application/json" - (Utils.Status.to_json status) `OK) + (Utils.Status.to_json status) + `OK) | Error (`Msg err) -> let status = { @@ -284,7 +293,8 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `Bad_request)))) + (Utils.Status.to_json status) + `Bad_request)))) let login store reqd = decode_request_body reqd >>= fun data -> @@ -305,7 +315,8 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `Bad_request) + (Utils.Status.to_json status) + `Bad_request) | Ok json -> ( let validate_user_input ~email ~password = if email = "" || password = "" then Error "All fields must be filled." @@ -333,7 +344,8 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `Bad_request) + (Utils.Status.to_json status) + `Bad_request) | Ok _ -> ( let now = Ptime.v (P.now_d_ps ()) in let _, (t : Storage.t) = !store in @@ -351,7 +363,8 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `Bad_request) + (Utils.Status.to_json status) + `Bad_request) | Ok user -> ( Store.update_user !store user >>= function | Ok store' -> ( @@ -386,7 +399,8 @@ struct Lwt.return (reply reqd ~header_list ~content_type:"application/json" - (Utils.Status.to_json status) `OK) + (Utils.Status.to_json status) + `OK) | None -> let status = { @@ -400,7 +414,8 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `Bad_request)) + (Utils.Status.to_json status) + `Bad_request)) | Error (`Msg msg) -> let status = { @@ -412,7 +427,8 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `Bad_request)))) + (Utils.Status.to_json status) + `Bad_request)))) let verify_email store reqd user = let email_verification_uuid = User_model.generate_uuid () in @@ -430,7 +446,8 @@ struct Logs.info (fun m -> m "Verification link is: %s" verification_link); Lwt.return (reply reqd ~content_type:"text/html" - (Verify_email.verify_page ~user ~icon:"/images/robur.png" ()) `OK) + (Verify_email.verify_page ~user ~icon:"/images/robur.png" ()) + `OK) | Error (`Msg msg) -> let status = { @@ -442,7 +459,8 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `Internal_server_error) + (Utils.Status.to_json status) + `Internal_server_error) let verify_email_token store reqd verification_token (user : User_model.user) = @@ -470,7 +488,8 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `Internal_server_error) + (Utils.Status.to_json status) + `Internal_server_error) else let status = { @@ -482,7 +501,8 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `Bad_request) + (Utils.Status.to_json status) + `Bad_request) | Error (`Msg s) -> Middleware.redirect_to_login reqd ~msg:s () let toggle_user store reqd _user = @@ -504,7 +524,8 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `Bad_request) + (Utils.Status.to_json status) + `Bad_request) | Ok (`Assoc json) -> ( match Utils.Json.get "uuid" json with | Some (`String uuid) -> ( @@ -523,7 +544,8 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `Not_found) + (Utils.Status.to_json status) + `Not_found) | Some user -> ( let is_last_active_user = user.active @@ -544,7 +566,8 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `Forbidden) + (Utils.Status.to_json status) + `Forbidden) else let user = User_model.update_user user ~active:(not user.active) @@ -564,7 +587,8 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `OK) + (Utils.Status.to_json status) + `OK) | Error (`Msg msg) -> let status = { @@ -576,7 +600,8 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `Internal_server_error))) + (Utils.Status.to_json status) + `Internal_server_error))) | _ -> Logs.warn (fun m -> m "Failed to parse JSON - no UUID found"); let status = @@ -589,7 +614,8 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `Not_found)) + (Utils.Status.to_json status) + `Not_found)) | Ok _ -> let status = { @@ -601,7 +627,8 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `Bad_request) + (Utils.Status.to_json status) + `Bad_request) let dashboard albatross reqd (user : User_model.user) = (* TODO use uuid in the future *) @@ -626,7 +653,8 @@ struct ~content: (Unikernel_index.unikernel_index_layout unikernels (Ptime.v (P.now_d_ps ()))) - ~icon:"/images/robur.png" ()) `OK) + ~icon:"/images/robur.png" ()) + `OK) let users store reqd user = Lwt.return @@ -635,7 +663,8 @@ struct ~content: (Users_index.users_index_layout (snd store).Storage.users (Ptime.v (P.now_d_ps ()))) - ~icon:"/images/robur.png" ()) `OK) + ~icon:"/images/robur.png" ()) + `OK) let settings store reqd user = Lwt.return @@ -643,7 +672,8 @@ struct (Dashboard.dashboard_layout user ~page_title:"Settings | Mollymawk" ~content: (Settings_page.settings_layout (snd store).Storage.configuration) - ~icon:"/images/robur.png" ()) `OK) + ~icon:"/images/robur.png" ()) + `OK) let update_settings stack store albatross reqd _user = decode_request_body reqd >>= fun data -> @@ -664,7 +694,8 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `Bad_request) + (Utils.Status.to_json status) + `Bad_request) | Ok json -> ( match Configuration.of_json_from_http json (Ptime.v (P.now_d_ps ())) @@ -690,7 +721,8 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `OK) + (Utils.Status.to_json status) + `OK) | Error (`Msg err) -> let status = { @@ -702,7 +734,8 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `Internal_server_error)) + (Utils.Status.to_json status) + `Internal_server_error)) | Error (`Msg err) -> let status = { @@ -714,7 +747,8 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `Internal_server_error)) + (Utils.Status.to_json status) + `Internal_server_error)) let deploy_form reqd user = Lwt.return @@ -722,7 +756,8 @@ struct (Dashboard.dashboard_layout user ~page_title:"Deploy a Unikernel | Mollymawk" ~content:Unikernel_create.unikernel_create_layout - ~icon:"/images/robur.png" ()) `OK) + ~icon:"/images/robur.png" ()) + `OK) let unikernel_info albatross reqd (user : User_model.user) = (* TODO use uuid in the future *) @@ -740,7 +775,8 @@ struct } in reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `Internal_server_error + (Utils.Status.to_json status) + `Internal_server_error | Ok (_hdr, res) -> ( match Albatross_json.res res with | Ok res -> @@ -753,7 +789,8 @@ struct } in reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `OK + (Utils.Status.to_json status) + `OK | Error (`String res) -> let status = { @@ -764,7 +801,8 @@ struct } in reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `Internal_server_error) + (Utils.Status.to_json status) + `Internal_server_error) let unikernel_info_one albatross name reqd (user : User_model.user) = (* TODO use uuid in the future *) @@ -790,7 +828,8 @@ struct ~content: (Unikernel_single.unikernel_single_layout (List.hd unikernels) (Ptime.v (P.now_d_ps ()))) - ~icon:"/images/robur.png" ()) `OK) + ~icon:"/images/robur.png" ()) + `OK) else let error = { @@ -802,9 +841,11 @@ struct in Lwt.return (reply reqd ~content_type:"text/html" - (Dashboard.dashboard_layout user ~page_title:"An Error Occured | Mollymawk" + (Dashboard.dashboard_layout user + ~page_title:"An Error Occured | Mollymawk" ~content:(Error_page.error_layout error) - ~icon:"/images/robur.png" ()) `Internal_server_error) + ~icon:"/images/robur.png" ()) + `Internal_server_error) let unikernel_destroy albatross name reqd (user : User_model.user) = (* TODO use uuid in the future *) @@ -822,7 +863,8 @@ struct } in reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `Internal_server_error + (Utils.Status.to_json status) + `Internal_server_error | Ok (_hdr, res) -> ( match Albatross_json.res res with | Ok res -> @@ -835,7 +877,8 @@ struct } in reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `OK + (Utils.Status.to_json status) + `OK | Error (`String res) -> let status = { @@ -846,7 +889,8 @@ struct } in reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `Internal_server_error) + (Utils.Status.to_json status) + `Internal_server_error) let unikernel_create albatross reqd (user : User_model.user) = let response_body = Httpaf.Reqd.request_body reqd in @@ -883,7 +927,8 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `Bad_request) + (Utils.Status.to_json status) + `Bad_request) | Ok ct -> ( match Multipart_form.of_string_to_list data ct with | Error (`Msg msg) -> @@ -898,7 +943,8 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `Bad_request) + (Utils.Status.to_json status) + `Bad_request) | Ok (m, assoc) -> ( let m, _r = to_map ~assoc m in match @@ -929,7 +975,8 @@ struct } in reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `Internal_server_error + (Utils.Status.to_json status) + `Internal_server_error | Ok (_hdr, res) -> ( match Albatross_json.res res with | Ok res -> @@ -942,7 +989,8 @@ struct } in reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `OK + (Utils.Status.to_json status) + `OK | Error (`String res) -> let status = { @@ -953,7 +1001,8 @@ struct } in reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `Internal_server_error)) + (Utils.Status.to_json status) + `Internal_server_error)) | Error (`Msg msg) -> Logs.warn (fun m -> m "couldn't decode data %s" msg); let status = @@ -966,7 +1015,8 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `Internal_server_error)) + (Utils.Status.to_json status) + `Internal_server_error)) | _ -> Logs.warn (fun m -> m "couldn't find fields"); let status = @@ -979,7 +1029,8 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `Bad_request))) + (Utils.Status.to_json status) + `Bad_request))) let unikernel_console albatross name reqd (user : User_model.user) = (* TODO use uuid in the future *) @@ -998,7 +1049,8 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `Internal_server_error) + (Utils.Status.to_json status) + `Internal_server_error) | Ok _ -> ( match Result.bind (Vmm_core.Name.path_of_string user.name) (fun domain -> @@ -1011,7 +1063,8 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Yojson.Basic.to_string (`List data)) `OK) + (Yojson.Basic.to_string (`List data)) + `OK) | Error (`Msg msg) -> let status = { @@ -1023,7 +1076,8 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `Internal_server_error)) + (Utils.Status.to_json status) + `Internal_server_error)) let request_handler stack albatross js_file css_file imgs store (_ipaddr, _port) reqd = @@ -1039,7 +1093,8 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `Bad_request) + (Utils.Status.to_json status) + `Bad_request) in let req = Httpaf.Reqd.request reqd in let path = @@ -1054,7 +1109,8 @@ struct (Guest_layout.guest_layout ~page_title:"Deploy unikernels with ease | Mollymawk" ~content:Index_page.index_page ~icon:"/images/robur.png" - ()) `OK)) + ()) + `OK)) | "/main.js" -> check_meth `GET (fun () -> Lwt.return (reply reqd ~content_type:"text/plain" js_file `OK)) @@ -1076,7 +1132,8 @@ struct (reply reqd ~content_type:"image/png" imgs.mirage_img `OK)) | "/images/robur.png" -> check_meth `GET (fun () -> - Lwt.return (reply reqd ~content_type:"image/png" imgs.robur_img `OK)) + Lwt.return + (reply reqd ~content_type:"image/png" imgs.robur_img `OK)) | "/style.css" -> check_meth `GET (fun () -> Lwt.return (reply reqd ~content_type:"text/css" css_file `OK)) @@ -1163,7 +1220,8 @@ struct (reply reqd ~content_type:"text/html" (Guest_layout.guest_layout ~page_title:"404 | Mollymawk" ~content:(Error_page.error_layout error) - ~icon:"/images/robur.png" ()) `Not_found)) + ~icon:"/images/robur.png" ()) + `Not_found)) let pp_error ppf = function | #Httpaf.Status.t as code -> Httpaf.Status.pp_hum ppf code From ff03148be591971b531f145629bc547ec6878a7d Mon Sep 17 00:00:00 2001 From: Hannes Mehnert Date: Fri, 6 Sep 2024 08:34:56 +0200 Subject: [PATCH 08/21] stick mirage to < 4.7.0 --- .cirrus.yml | 2 +- config.ml | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.cirrus.yml b/.cirrus.yml index 782a83ad..412e7a54 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -4,7 +4,7 @@ freebsd_instance: freebsd_task: pkg_install_script: pkg install -y ocaml-opam gmake bash ocaml_script: opam init -a --comp=4.14.2 - mirage_script: eval `opam env` && opam install --confirm-level=unsafe-yes "mirage>=4.5.0" + mirage_script: eval `opam env` && opam install --confirm-level=unsafe-yes "mirage<4.7.0" configure_script: eval `opam env` && mirage configure -t hvt depend_script: eval `opam env` && gmake depend build_script: eval `opam env` && gmake build diff --git a/config.ml b/config.ml index 01565b29..6d9f1dee 100644 --- a/config.ml +++ b/config.ml @@ -1,3 +1,4 @@ +(* mirage >= 4.6.0 & < 4.7.0 *) open Mirage let assets = crunch "assets" @@ -8,7 +9,7 @@ let mollymawk = package "logs"; package "x509"; package "tls-mirage"; - package ~min:"2.1.0" "albatross"; + package ~min:"2.1.0" ~max:"2.2.0" "albatross"; package "yojson"; package "uri"; package "tyxml"; From bcb66535d9c4c54cdace8c728f212a130a7ae9f2 Mon Sep 17 00:00:00 2001 From: PizieDust Date: Sat, 7 Sep 2024 09:16:56 +0200 Subject: [PATCH 09/21] show active admins --- users_index.ml | 63 +++++++++++++++++++++++++------------------------- 1 file changed, 31 insertions(+), 32 deletions(-) diff --git a/users_index.ml b/users_index.ml index e17af011..d5b83e1f 100644 --- a/users_index.ml +++ b/users_index.ml @@ -142,43 +142,42 @@ let users_index_layout (users : User_model.user list) current_time = ]; ] [ - p [ txt user.name ]; - (if user.super_user then - i - ~a: - [ - a_class - [ - "text-primary-800 \ - fa-solid \ - fa-check-double"; - ]; - ] - [] - else - match user.active with - | true -> - i - ~a: - [ - a_class - [ - "text-primary-500 \ - fa-solid fa-check"; - ]; - ] - [] - | false -> + div + [ + p [ txt user.name ]; + (if user.super_user then i ~a: [ a_class - [ - "text-secondary-500 \ - fa-solid fa-x"; - ]; + [ "txt-primary-500" ]; ] - []); + [ txt "administrator" ] + else p []); + ]; + (match user.active with + | true -> + i + ~a: + [ + a_class + [ + "text-primary-500 \ + fa-solid fa-check"; + ]; + ] + [] + | false -> + i + ~a: + [ + a_class + [ + "text-secondary-500 \ + fa-solid fa-x"; + ]; + ] + []); ]; ]; td From 43cb725b8324cc7dec5e59be49a58f498aaabd56 Mon Sep 17 00:00:00 2001 From: PizieDust Date: Sat, 7 Sep 2024 09:17:26 +0200 Subject: [PATCH 10/21] lint --- unikernel.ml | 177 ++++++++++++++++++++++++++++++++++----------------- 1 file changed, 118 insertions(+), 59 deletions(-) diff --git a/unikernel.ml b/unikernel.ml index 3dbae121..3d447656 100644 --- a/unikernel.ml +++ b/unikernel.ml @@ -123,7 +123,8 @@ struct assert false) reqd - let reply reqd ?(content_type = "text/plain") ?(header_list = []) data status = + let reply reqd ?(content_type = "text/plain") ?(header_list = []) data status + = let h = Httpaf.Headers.of_list [ @@ -142,12 +143,14 @@ struct | Ok "" -> Lwt.return (reply reqd ~content_type:"text/html" - (Sign_up.register_page ~icon:"/images/robur.png" ()) `OK) + (Sign_up.register_page ~icon:"/images/robur.png" ()) + `OK) | _ -> Middleware.redirect_to_dashboard reqd ()) | None -> Lwt.return (reply reqd ~content_type:"text/html" - (Sign_up.register_page ~icon:"/images/robur.png" ()) `OK) + (Sign_up.register_page ~icon:"/images/robur.png" ()) + `OK) let sign_in reqd = match Middleware.has_session_cookie reqd with @@ -156,12 +159,14 @@ struct | Ok "" -> Lwt.return (reply reqd ~content_type:"text/html" - (Sign_in.login_page ~icon:"/images/robur.png" ()) `OK) + (Sign_in.login_page ~icon:"/images/robur.png" ()) + `OK) | _ -> Middleware.redirect_to_dashboard reqd ()) | None -> Lwt.return (reply reqd ~content_type:"text/html" - (Sign_in.login_page ~icon:"/images/robur.png" ()) `OK) + (Sign_in.login_page ~icon:"/images/robur.png" ()) + `OK) let register store reqd = decode_request_body reqd >>= fun data -> @@ -182,7 +187,8 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `Bad_request) + (Utils.Status.to_json status) + `Bad_request) | Ok json -> ( let validate_user_input ~name ~email ~password = if name = "" || email = "" || password = "" then @@ -216,7 +222,8 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `Bad_request) + (Utils.Status.to_json status) + `Bad_request) | Ok _ -> ( let _, (s : Storage.t) = !store in let users = s.users in @@ -233,7 +240,8 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `Bad_request) + (Utils.Status.to_json status) + `Bad_request) | None -> ( let created_at = Ptime.v (P.now_d_ps ()) in let user = @@ -272,7 +280,8 @@ struct in Lwt.return (reply reqd ~header_list ~content_type:"application/json" - (Utils.Status.to_json status) `OK) + (Utils.Status.to_json status) + `OK) | Error (`Msg err) -> let status = { @@ -284,7 +293,8 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `Bad_request)))) + (Utils.Status.to_json status) + `Bad_request)))) let login store reqd = decode_request_body reqd >>= fun data -> @@ -305,7 +315,8 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `Bad_request) + (Utils.Status.to_json status) + `Bad_request) | Ok json -> ( let validate_user_input ~email ~password = if email = "" || password = "" then Error "All fields must be filled." @@ -333,7 +344,8 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `Bad_request) + (Utils.Status.to_json status) + `Bad_request) | Ok _ -> ( let now = Ptime.v (P.now_d_ps ()) in let _, (t : Storage.t) = !store in @@ -351,7 +363,8 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `Bad_request) + (Utils.Status.to_json status) + `Bad_request) | Ok user -> ( Store.update_user !store user >>= function | Ok store' -> ( @@ -386,7 +399,8 @@ struct Lwt.return (reply reqd ~header_list ~content_type:"application/json" - (Utils.Status.to_json status) `OK) + (Utils.Status.to_json status) + `OK) | None -> let status = { @@ -400,7 +414,8 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `Bad_request)) + (Utils.Status.to_json status) + `Bad_request)) | Error (`Msg msg) -> let status = { @@ -412,7 +427,8 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `Bad_request)))) + (Utils.Status.to_json status) + `Bad_request)))) let verify_email store reqd user = let email_verification_uuid = User_model.generate_uuid () in @@ -430,7 +446,8 @@ struct Logs.info (fun m -> m "Verification link is: %s" verification_link); Lwt.return (reply reqd ~content_type:"text/html" - (Verify_email.verify_page ~user ~icon:"/images/robur.png" ()) `OK) + (Verify_email.verify_page ~user ~icon:"/images/robur.png" ()) + `OK) | Error (`Msg msg) -> let status = { @@ -442,7 +459,8 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `Internal_server_error) + (Utils.Status.to_json status) + `Internal_server_error) let verify_email_token store reqd verification_token (user : User_model.user) = @@ -470,7 +488,8 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `Internal_server_error) + (Utils.Status.to_json status) + `Internal_server_error) else let status = { @@ -482,7 +501,8 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `Bad_request) + (Utils.Status.to_json status) + `Bad_request) | Error (`Msg s) -> Middleware.redirect_to_login reqd ~msg:s () let toggle_user store reqd _user = @@ -504,7 +524,8 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `Bad_request) + (Utils.Status.to_json status) + `Bad_request) | Ok (`Assoc json) -> ( match Utils.Json.get "uuid" json with | Some (`String uuid) -> ( @@ -523,7 +544,8 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `Not_found) + (Utils.Status.to_json status) + `Not_found) | Some user -> ( let is_last_active_user = user.active @@ -544,7 +566,8 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `Forbidden) + (Utils.Status.to_json status) + `Forbidden) else let user = User_model.update_user user ~active:(not user.active) @@ -564,7 +587,8 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `OK) + (Utils.Status.to_json status) + `OK) | Error (`Msg msg) -> let status = { @@ -576,7 +600,8 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `Internal_server_error))) + (Utils.Status.to_json status) + `Internal_server_error))) | _ -> Logs.warn (fun m -> m "Failed to parse JSON - no UUID found"); let status = @@ -589,7 +614,8 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `Not_found)) + (Utils.Status.to_json status) + `Not_found)) | Ok _ -> let status = { @@ -601,7 +627,8 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `Bad_request) + (Utils.Status.to_json status) + `Bad_request) let dashboard albatross reqd (user : User_model.user) = (* TODO use uuid in the future *) @@ -626,7 +653,8 @@ struct ~content: (Unikernel_index.unikernel_index_layout unikernels (Ptime.v (P.now_d_ps ()))) - ~icon:"/images/robur.png" ()) `OK) + ~icon:"/images/robur.png" ()) + `OK) let users store reqd user = Lwt.return @@ -635,7 +663,8 @@ struct ~content: (Users_index.users_index_layout (snd store).Storage.users (Ptime.v (P.now_d_ps ()))) - ~icon:"/images/robur.png" ()) `OK) + ~icon:"/images/robur.png" ()) + `OK) let settings store reqd user = Lwt.return @@ -643,7 +672,8 @@ struct (Dashboard.dashboard_layout user ~page_title:"Settings | Mollymawk" ~content: (Settings_page.settings_layout (snd store).Storage.configuration) - ~icon:"/images/robur.png" ()) `OK) + ~icon:"/images/robur.png" ()) + `OK) let update_settings stack store albatross reqd _user = decode_request_body reqd >>= fun data -> @@ -664,7 +694,8 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `Bad_request) + (Utils.Status.to_json status) + `Bad_request) | Ok json -> ( match Configuration.of_json_from_http json (Ptime.v (P.now_d_ps ())) @@ -690,7 +721,8 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `OK) + (Utils.Status.to_json status) + `OK) | Error (`Msg err) -> let status = { @@ -702,7 +734,8 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `Internal_server_error)) + (Utils.Status.to_json status) + `Internal_server_error)) | Error (`Msg err) -> let status = { @@ -714,7 +747,8 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `Internal_server_error)) + (Utils.Status.to_json status) + `Internal_server_error)) let deploy_form reqd user = Lwt.return @@ -722,7 +756,8 @@ struct (Dashboard.dashboard_layout user ~page_title:"Deploy a Unikernel | Mollymawk" ~content:Unikernel_create.unikernel_create_layout - ~icon:"/images/robur.png" ()) `OK) + ~icon:"/images/robur.png" ()) + `OK) let unikernel_info albatross reqd (user : User_model.user) = (* TODO use uuid in the future *) @@ -740,7 +775,8 @@ struct } in reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `Internal_server_error + (Utils.Status.to_json status) + `Internal_server_error | Ok (_hdr, res) -> ( match Albatross_json.res res with | Ok res -> @@ -753,7 +789,8 @@ struct } in reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `OK + (Utils.Status.to_json status) + `OK | Error (`String res) -> let status = { @@ -764,7 +801,8 @@ struct } in reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `Internal_server_error) + (Utils.Status.to_json status) + `Internal_server_error) let unikernel_info_one albatross name reqd (user : User_model.user) = (* TODO use uuid in the future *) @@ -790,7 +828,8 @@ struct ~content: (Unikernel_single.unikernel_single_layout (List.hd unikernels) (Ptime.v (P.now_d_ps ()))) - ~icon:"/images/robur.png" ()) `OK) + ~icon:"/images/robur.png" ()) + `OK) else let error = { @@ -802,9 +841,11 @@ struct in Lwt.return (reply reqd ~content_type:"text/html" - (Dashboard.dashboard_layout user ~page_title:"An Error Occured | Mollymawk" + (Dashboard.dashboard_layout user + ~page_title:"An Error Occured | Mollymawk" ~content:(Error_page.error_layout error) - ~icon:"/images/robur.png" ()) `Internal_server_error) + ~icon:"/images/robur.png" ()) + `Internal_server_error) let unikernel_destroy albatross name reqd (user : User_model.user) = (* TODO use uuid in the future *) @@ -822,7 +863,8 @@ struct } in reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `Internal_server_error + (Utils.Status.to_json status) + `Internal_server_error | Ok (_hdr, res) -> ( match Albatross_json.res res with | Ok res -> @@ -835,7 +877,8 @@ struct } in reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `OK + (Utils.Status.to_json status) + `OK | Error (`String res) -> let status = { @@ -846,7 +889,8 @@ struct } in reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `Internal_server_error) + (Utils.Status.to_json status) + `Internal_server_error) let unikernel_create albatross reqd (user : User_model.user) = let response_body = Httpaf.Reqd.request_body reqd in @@ -883,7 +927,8 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `Bad_request) + (Utils.Status.to_json status) + `Bad_request) | Ok ct -> ( match Multipart_form.of_string_to_list data ct with | Error (`Msg msg) -> @@ -898,7 +943,8 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `Bad_request) + (Utils.Status.to_json status) + `Bad_request) | Ok (m, assoc) -> ( let m, _r = to_map ~assoc m in match @@ -929,7 +975,8 @@ struct } in reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `Internal_server_error + (Utils.Status.to_json status) + `Internal_server_error | Ok (_hdr, res) -> ( match Albatross_json.res res with | Ok res -> @@ -942,7 +989,8 @@ struct } in reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `OK + (Utils.Status.to_json status) + `OK | Error (`String res) -> let status = { @@ -953,7 +1001,8 @@ struct } in reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `Internal_server_error)) + (Utils.Status.to_json status) + `Internal_server_error)) | Error (`Msg msg) -> Logs.warn (fun m -> m "couldn't decode data %s" msg); let status = @@ -966,7 +1015,8 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `Internal_server_error)) + (Utils.Status.to_json status) + `Internal_server_error)) | _ -> Logs.warn (fun m -> m "couldn't find fields"); let status = @@ -979,7 +1029,8 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `Bad_request))) + (Utils.Status.to_json status) + `Bad_request))) let unikernel_console albatross name reqd (user : User_model.user) = (* TODO use uuid in the future *) @@ -998,7 +1049,8 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `Internal_server_error) + (Utils.Status.to_json status) + `Internal_server_error) | Ok _ -> ( match Result.bind (Vmm_core.Name.path_of_string user.name) (fun domain -> @@ -1011,7 +1063,8 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Yojson.Basic.to_string (`List data)) `OK) + (Yojson.Basic.to_string (`List data)) + `OK) | Error (`Msg msg) -> let status = { @@ -1023,7 +1076,8 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `Internal_server_error)) + (Utils.Status.to_json status) + `Internal_server_error)) let request_handler stack albatross js_file css_file imgs store (_ipaddr, _port) reqd = @@ -1039,7 +1093,8 @@ struct in Lwt.return (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) `Bad_request) + (Utils.Status.to_json status) + `Bad_request) in let req = Httpaf.Reqd.request reqd in let path = @@ -1054,10 +1109,12 @@ struct (Guest_layout.guest_layout ~page_title:"Deploy unikernels with ease | Mollymawk" ~content:Index_page.index_page ~icon:"/images/robur.png" - ()) `OK)) + ()) + `OK)) | "/main.js" -> check_meth `GET (fun () -> - Lwt.return (reply reqd ~content_type:"text/plain" js_file `OK)) + Lwt.return + (reply reqd ~content_type:"text/javascript" js_file `OK)) | "/images/molly_bird.jpeg" -> check_meth `GET (fun () -> Lwt.return @@ -1076,7 +1133,8 @@ struct (reply reqd ~content_type:"image/png" imgs.mirage_img `OK)) | "/images/robur.png" -> check_meth `GET (fun () -> - Lwt.return (reply reqd ~content_type:"image/png" imgs.robur_img `OK)) + Lwt.return + (reply reqd ~content_type:"image/png" imgs.robur_img `OK)) | "/style.css" -> check_meth `GET (fun () -> Lwt.return (reply reqd ~content_type:"text/css" css_file `OK)) @@ -1163,7 +1221,8 @@ struct (reply reqd ~content_type:"text/html" (Guest_layout.guest_layout ~page_title:"404 | Mollymawk" ~content:(Error_page.error_layout error) - ~icon:"/images/robur.png" ()) `Not_found)) + ~icon:"/images/robur.png" ()) + `Not_found)) let pp_error ppf = function | #Httpaf.Status.t as code -> Httpaf.Status.pp_hum ppf code From b00a22b0852363f9c87e865026597d1aec3dd0a3 Mon Sep 17 00:00:00 2001 From: PizieDust Date: Sat, 7 Sep 2024 09:23:38 +0200 Subject: [PATCH 11/21] remove admin middleware --- unikernel.ml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/unikernel.ml b/unikernel.ml index 3d447656..98c61410 100644 --- a/unikernel.ml +++ b/unikernel.ml @@ -1171,9 +1171,9 @@ struct check_meth `POST (fun () -> authenticate ~check_admin:true ~api_meth:true !store reqd (toggle_user store reqd)) - | "/api/admin/unikernels" -> + | "/api/unikernels" -> check_meth `GET (fun () -> - authenticate ~check_admin:true ~api_meth:true !store reqd + authenticate ~api_meth:true !store reqd (unikernel_info !albatross reqd)) | path when String.(length path >= 16 && sub path 0 16 = "/unikernel/info/") From 67de9cb866c368c877aac3453eb63ee8e2d06b05 Mon Sep 17 00:00:00 2001 From: PizieDust Date: Sat, 7 Sep 2024 09:45:08 +0200 Subject: [PATCH 12/21] better ui --- users_index.ml | 82 ++++++++++++++++++++++++++------------------------ 1 file changed, 43 insertions(+), 39 deletions(-) diff --git a/users_index.ml b/users_index.ml index d5b83e1f..2796807d 100644 --- a/users_index.ml +++ b/users_index.ml @@ -133,51 +133,55 @@ let users_index_layout (users : User_model.user list) current_time = ] [ div - ~a: - [ - a_class - [ - "flex justify-start space-x-1 \ - items-center"; - ]; - ] [ div + ~a: + [ + a_class + [ + "flex justify-start \ + space-x-1 items-center"; + ]; + ] [ p [ txt user.name ]; - (if user.super_user then - i - ~a: - [ - a_class - [ "txt-primary-500" ]; - ] - [ txt "administrator" ] - else p []); - ]; - (match user.active with - | true -> - i - ~a: - [ - a_class + (match user.active with + | true -> + i + ~a: [ - "text-primary-500 \ - fa-solid fa-check"; - ]; - ] - [] - | false -> - i - ~a: - [ - a_class + a_class + [ + "text-primary-500 \ + fa-solid \ + fa-check"; + ]; + ] + [] + | false -> + i + ~a: [ - "text-secondary-500 \ - fa-solid fa-x"; - ]; - ] - []); + a_class + [ + "text-secondary-500 \ + fa-solid fa-x"; + ]; + ] + []); + ]; + (if user.super_user then + i + ~a: + [ + a_class + [ + "text-primary-500 \ + lowercase text-sm"; + ]; + ] + [ txt "admin" ] + else p []); ]; ]; td From b1885ff8773379a46010dc0cc3190df2b8889712 Mon Sep 17 00:00:00 2001 From: PizieDust Date: Sat, 7 Sep 2024 09:45:15 +0200 Subject: [PATCH 13/21] update styles --- assets/style.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/style.css b/assets/style.css index 839d5653..d46740b6 100755 --- a/assets/style.css +++ b/assets/style.css @@ -1 +1 @@ -/*! tailwindcss v3.4.4 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,::backdrop,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.pointer-events-none{pointer-events:none}.pointer-events-auto{pointer-events:auto}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{inset:0}.inset-1{inset:.25rem}.-inset-x-12{left:-3rem;right:-3rem}.inset-x-1{left:.25rem;right:.25rem}.inset-x-4{left:1rem;right:1rem}.-bottom-0{bottom:0}.-bottom-0\.5{bottom:-.125rem}.-bottom-2{bottom:-.5rem}.-bottom-6{bottom:-1.5rem}.-left-1\/4{left:-25%}.-left-2{left:-.5rem}.-top-1\/4{top:-25%}.-top-4{top:-1rem}.-top-6{top:-1.5rem}.bottom-0{bottom:0}.bottom-10{bottom:2.5rem}.bottom-6{bottom:1.5rem}.left-0{left:0}.left-1\/2{left:50%}.left-8{left:2rem}.right-4{right:1rem}.top-0{top:0}.top-1\/4{top:25%}.z-50{z-index:50}.z-\[-1\]{z-index:-1}.z-\[500\]{z-index:500}.col-span-1{grid-column:span 1/span 1}.col-span-10{grid-column:span 10/span 10}.col-span-2{grid-column:span 2/span 2}.col-span-3{grid-column:span 3/span 3}.col-span-4{grid-column:span 4/span 4}.col-span-7{grid-column:span 7/span 7}.-m-1{margin:-.25rem}.-m-1\.5{margin:-.375rem}.m-auto{margin:auto}.mx-auto{margin-left:auto;margin-right:auto}.my-1{margin-top:.25rem;margin-bottom:.25rem}.my-10{margin-top:2.5rem;margin-bottom:2.5rem}.my-2{margin-top:.5rem;margin-bottom:.5rem}.my-3{margin-top:.75rem;margin-bottom:.75rem}.my-4{margin-top:1rem;margin-bottom:1rem}.my-5{margin-top:1.25rem;margin-bottom:1.25rem}.my-6{margin-top:1.5rem;margin-bottom:1.5rem}.-mb-12{margin-bottom:-3rem}.-mb-6{margin-bottom:-1.5rem}.-ml-2{margin-left:-.5rem}.-mr-2{margin-right:-.5rem}.-mr-2\.5{margin-right:-.625rem}.-mt-1{margin-top:-.25rem}.-mt-24{margin-top:-6rem}.-mt-3{margin-top:-.75rem}.mb-0{margin-bottom:0}.mb-14{margin-bottom:3.5rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-7{margin-bottom:1.75rem}.mb-8{margin-bottom:2rem}.mb-9{margin-bottom:2.25rem}.mb-auto{margin-bottom:auto}.mb-px{margin-bottom:1px}.ml-5{margin-left:1.25rem}.mr-2{margin-right:.5rem}.mr-3{margin-right:.75rem}.mt-1{margin-top:.25rem}.mt-1\.5{margin-top:.375rem}.mt-10{margin-top:2.5rem}.mt-12{margin-top:3rem}.mt-16{margin-top:4rem}.mt-2{margin-top:.5rem}.mt-4{margin-top:1rem}.mt-8{margin-top:2rem}.mt-auto{margin-top:auto}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.size-2{width:.5rem;height:.5rem}.size-2\.5{width:.625rem;height:.625rem}.h-12{height:3rem}.h-2{height:.5rem}.h-2\.5{height:.625rem}.h-24{height:6rem}.h-32{height:8rem}.h-8{height:2rem}.h-96{height:24rem}.h-\[150\%\]{height:150%}.h-full{height:100%}.h-px{height:1px}.max-h-screen{max-height:100vh}.min-h-screen{min-height:100vh}.w-0{width:0}.w-14{width:3.5rem}.w-16{width:4rem}.w-32{width:8rem}.w-60{width:15rem}.w-\[150\%\]{width:150%}.w-fit{width:-moz-fit-content;width:fit-content}.w-full{width:100%}.w-max{width:-moz-max-content;width:max-content}.w-px{width:1px}.min-w-full{min-width:100%}.max-w-5xl{max-width:64rem}.max-w-6xl{max-width:72rem}.max-w-7xl{max-width:80rem}.max-w-\[94\%\]{max-width:94%}.max-w-full{max-width:100%}.max-w-lg{max-width:32rem}.max-w-md{max-width:28rem}.max-w-none{max-width:none}.max-w-sm{max-width:24rem}.max-w-xl{max-width:36rem}.flex-none{flex:none}.shrink-0{flex-shrink:0}.table-auto{table-layout:auto}.border-collapse{border-collapse:collapse}.origin-bottom{transform-origin:bottom}.-translate-x-1\/2{--tw-translate-x:-50%}.-rotate-3,.-translate-x-1\/2{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-rotate-3{--tw-rotate:-3deg}.rotate-3{--tw-rotate:3deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.scale-0{--tw-scale-x:0;--tw-scale-y:0}.scale-0,.scale-x-0{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.scale-x-0{--tw-scale-x:0}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.transform-gpu{transform:translate3d(var(--tw-translate-x),var(--tw-translate-y),0) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes spin{to{transform:rotate(1turn)}}.animate-spin{animation:spin 1s linear infinite}.cursor-pointer{cursor:pointer}.appearance-none{-webkit-appearance:none;-moz-appearance:none;appearance:none}.grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.flex-col{flex-direction:column}.place-items-center{place-items:center}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.justify-items-center{justify-items:center}.gap-0{gap:0}.gap-0\.5{gap:.125rem}.gap-10{gap:2.5rem}.gap-12{gap:3rem}.gap-20{gap:5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-8{gap:2rem}.gap-x-2{-moz-column-gap:.5rem;column-gap:.5rem}.gap-x-8{-moz-column-gap:2rem;column-gap:2rem}.gap-y-16{row-gap:4rem}.gap-y-4{row-gap:1rem}.space-x-1>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.25rem*var(--tw-space-x-reverse));margin-left:calc(.25rem*(1 - var(--tw-space-x-reverse)))}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.5rem*var(--tw-space-x-reverse));margin-left:calc(.5rem*(1 - var(--tw-space-x-reverse)))}.space-x-20>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(5rem*var(--tw-space-x-reverse));margin-left:calc(5rem*(1 - var(--tw-space-x-reverse)))}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem*var(--tw-space-x-reverse));margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)))}.space-x-5>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1.25rem*var(--tw-space-x-reverse));margin-left:calc(1.25rem*(1 - var(--tw-space-x-reverse)))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem*var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.75rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem*var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem*var(--tw-space-y-reverse))}.space-y-5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.25rem*var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem*var(--tw-space-y-reverse))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse:0;border-top-width:calc(1px*(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px*var(--tw-divide-y-reverse))}.divide-gray-200>:not([hidden])~:not([hidden]){--tw-divide-opacity:1;border-color:rgb(229 231 235/var(--tw-divide-opacity))}.justify-self-start{justify-self:start}.justify-self-end{justify-self:end}.overflow-hidden{overflow:hidden}.overflow-visible{overflow:visible}.overflow-x-auto{overflow-x:auto}.whitespace-normal{white-space:normal}.whitespace-nowrap{white-space:nowrap}.text-wrap{text-wrap:wrap}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-3xl{border-radius:1.5rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-xl{border-radius:.75rem}.rounded-l-\[20px\]{border-top-left-radius:20px;border-bottom-left-radius:20px}.rounded-r-\[20px\]{border-top-right-radius:20px;border-bottom-right-radius:20px}.rounded-r-\[8px\]{border-top-right-radius:8px;border-bottom-right-radius:8px}.rounded-t-full{border-top-left-radius:9999px;border-top-right-radius:9999px}.border{border-width:1px}.border-l-0{border-left-width:0}.border-r-0{border-right-width:0}.border-t-0{border-top-width:0}.border-none{border-style:none}.border-\[\#D7DFE9\]{--tw-border-opacity:1;border-color:rgb(215 223 233/var(--tw-border-opacity))}.border-primary-200{--tw-border-opacity:1;border-color:rgb(171 228 214/var(--tw-border-opacity))}.border-primary-400{--tw-border-opacity:1;border-color:rgb(78 179 161/var(--tw-border-opacity))}.border-primary-500{--tw-border-opacity:1;border-color:rgb(54 156 140/var(--tw-border-opacity))}.border-primary-600{--tw-border-opacity:1;border-color:rgb(40 121 110/var(--tw-border-opacity))}.border-primary-700{--tw-border-opacity:1;border-color:rgb(35 98 90/var(--tw-border-opacity))}.border-y-secondary-300{--tw-border-opacity:1;border-top-color:rgb(255 170 157/var(--tw-border-opacity));border-bottom-color:rgb(255 170 157/var(--tw-border-opacity))}.bg-gray-100{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity))}.bg-gray-500\/25{background-color:#6b728040}.bg-primary-100{--tw-bg-opacity:1;background-color:rgb(213 242 235/var(--tw-bg-opacity))}.bg-primary-200{--tw-bg-opacity:1;background-color:rgb(171 228 214/var(--tw-bg-opacity))}.bg-primary-300{--tw-bg-opacity:1;background-color:rgb(122 206 189/var(--tw-bg-opacity))}.bg-primary-50{--tw-bg-opacity:1;background-color:rgb(243 250 249/var(--tw-bg-opacity))}.bg-primary-500{--tw-bg-opacity:1;background-color:rgb(54 156 140/var(--tw-bg-opacity))}.bg-primary-800{--tw-bg-opacity:1;background-color:rgb(32 79 74/var(--tw-bg-opacity))}.bg-primary-950{--tw-bg-opacity:1;background-color:rgb(13 38 37/var(--tw-bg-opacity))}.bg-secondary-300{--tw-bg-opacity:1;background-color:rgb(255 170 157/var(--tw-bg-opacity))}.bg-secondary-500{--tw-bg-opacity:1;background-color:rgb(255 78 51/var(--tw-bg-opacity))}.bg-secondary-700{--tw-bg-opacity:1;background-color:rgb(200 38 13/var(--tw-bg-opacity))}.bg-transparent{background-color:initial}.bg-opacity-0{--tw-bg-opacity:0}.bg-gradient-to-b{background-image:linear-gradient(to bottom,var(--tw-gradient-stops))}.bg-gradient-to-br{background-image:linear-gradient(to bottom right,var(--tw-gradient-stops))}.bg-gradient-to-r{background-image:linear-gradient(to right,var(--tw-gradient-stops))}.bg-gradient-to-t{background-image:linear-gradient(to top,var(--tw-gradient-stops))}.from-gray-100{--tw-gradient-from:#f3f4f6 var(--tw-gradient-from-position);--tw-gradient-to:#f3f4f600 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-primary-900\/50{--tw-gradient-from:#1f423e80 var(--tw-gradient-from-position);--tw-gradient-to:#1f423e00 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-primary-950{--tw-gradient-from:#0d2625 var(--tw-gradient-from-position);--tw-gradient-to:#0d262500 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-transparent{--tw-gradient-from:#0000 var(--tw-gradient-from-position);--tw-gradient-to:#0000 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-10\%{--tw-gradient-from-position:10%}.via-primary-900{--tw-gradient-to:#1f423e00 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),#1f423e var(--tw-gradient-via-position),var(--tw-gradient-to)}.via-primary-900\/50{--tw-gradient-to:#1f423e00 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),#1f423e80 var(--tw-gradient-via-position),var(--tw-gradient-to)}.via-10\%{--tw-gradient-via-position:10%}.to-primary-950{--tw-gradient-to:#0d2625 var(--tw-gradient-to-position)}.to-primary-950\/50{--tw-gradient-to:#0d262580 var(--tw-gradient-to-position)}.to-transparent{--tw-gradient-to:#0000 var(--tw-gradient-to-position)}.to-90\%{--tw-gradient-to-position:90%}.bg-\[length\:100px_auto\]{background-size:100px auto}.bg-cover{background-size:cover}.bg-clip-padding{background-clip:padding-box}.bg-center{background-position:50%}.fill-primary-400{fill:#4eb3a1}.object-contain{-o-object-fit:contain;object-fit:contain}.object-cover{-o-object-fit:cover;object-fit:cover}.object-bottom{-o-object-position:bottom;object-position:bottom}.p-0{padding:0}.p-1{padding:.25rem}.p-1\.5{padding:.375rem}.p-10{padding:2.5rem}.p-16{padding:4rem}.p-2{padding:.5rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.px-10{padding-left:2.5rem;padding-right:2.5rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-40{padding-left:10rem;padding-right:10rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-0{padding-top:0;padding-bottom:0}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-10{padding-top:2.5rem;padding-bottom:2.5rem}.py-16{padding-top:4rem;padding-bottom:4rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-24{padding-top:6rem;padding-bottom:6rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-5{padding-top:1.25rem;padding-bottom:1.25rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.py-8{padding-top:2rem;padding-bottom:2rem}.pb-12{padding-bottom:3rem}.pb-16{padding-bottom:4rem}.pb-3{padding-bottom:.75rem}.pb-32{padding-bottom:8rem}.pb-8{padding-bottom:2rem}.pl-3{padding-left:.75rem}.pl-4{padding-left:1rem}.pr-3{padding-right:.75rem}.pr-7{padding-right:1.75rem}.pr-\[10px\]{padding-right:10px}.pt-12{padding-top:3rem}.pt-24{padding-top:6rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.text-start{text-align:start}.align-middle{vertical-align:middle}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-5xl{font-size:3rem;line-height:1}.text-7xl{font-size:4.5rem;line-height:1}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-\[450\]{font-weight:450}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.leading-6{line-height:1.5rem}.leading-none{line-height:1}.tracking-tight{letter-spacing:-.025em}.tracking-wider{letter-spacing:.05em}.text-gray-100{--tw-text-opacity:1;color:rgb(243 244 246/var(--tw-text-opacity))}.text-gray-200{--tw-text-opacity:1;color:rgb(229 231 235/var(--tw-text-opacity))}.text-gray-50{--tw-text-opacity:1;color:rgb(249 250 251/var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.text-gray-600{--tw-text-opacity:1;color:rgb(75 85 99/var(--tw-text-opacity))}.text-gray-700{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity))}.text-gray-800{--tw-text-opacity:1;color:rgb(31 41 55/var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity:1;color:rgb(17 24 39/var(--tw-text-opacity))}.text-primary-300{--tw-text-opacity:1;color:rgb(122 206 189/var(--tw-text-opacity))}.text-primary-400{--tw-text-opacity:1;color:rgb(78 179 161/var(--tw-text-opacity))}.text-primary-50{--tw-text-opacity:1;color:rgb(243 250 249/var(--tw-text-opacity))}.text-primary-500{--tw-text-opacity:1;color:rgb(54 156 140/var(--tw-text-opacity))}.text-primary-600{--tw-text-opacity:1;color:rgb(40 121 110/var(--tw-text-opacity))}.text-primary-800{--tw-text-opacity:1;color:rgb(32 79 74/var(--tw-text-opacity))}.text-primary-900{--tw-text-opacity:1;color:rgb(31 66 62/var(--tw-text-opacity))}.text-secondary-300{--tw-text-opacity:1;color:rgb(255 170 157/var(--tw-text-opacity))}.text-secondary-50{--tw-text-opacity:1;color:rgb(255 243 241/var(--tw-text-opacity))}.text-secondary-500{--tw-text-opacity:1;color:rgb(255 78 51/var(--tw-text-opacity))}.text-primary-950{--tw-text-opacity:1;color:rgb(13 38 37/var(--tw-text-opacity))}.opacity-0{opacity:0}.opacity-50{opacity:.5}.shadow{--tw-shadow:0 1px 3px 0 #0000001a,0 1px 2px -1px #0000001a;--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color)}.shadow,.shadow-lg{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a;--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}.shadow-md{--tw-shadow:0 4px 6px -1px #0000001a,0 2px 4px -2px #0000001a;--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color)}.shadow-md,.shadow-sm{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 2px 0 #0000000d;--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color)}.shadow-gray-800\/5{--tw-shadow-color:#1f29370d;--tw-shadow:var(--tw-shadow-colored)}.outline-0{outline-width:0}.ring-1{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.ring-inset{--tw-ring-inset:inset}.ring-gray-800\/\[\.075\]{--tw-ring-color:rgba(31,41,55,.075)}.ring-primary-100{--tw-ring-opacity:1;--tw-ring-color:rgb(213 242 235/var(--tw-ring-opacity))}.ring-primary-200{--tw-ring-opacity:1;--tw-ring-color:rgb(171 228 214/var(--tw-ring-opacity))}.blur{--tw-blur:blur(8px)}.blur,.blur-3xl{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.blur-3xl{--tw-blur:blur(64px)}.brightness-0{--tw-brightness:brightness(0)}.brightness-0,.drop-shadow{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.drop-shadow{--tw-drop-shadow:drop-shadow(0 1px 2px #0000001a) drop-shadow(0 1px 1px #0000000f)}.drop-shadow-sm{--tw-drop-shadow:drop-shadow(0 1px 1px #0000000d)}.drop-shadow-sm,.invert{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.invert{--tw-invert:invert(100%)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur-sm{--tw-backdrop-blur:blur(4px)}.backdrop-blur-sm,.backdrop-blur-xl{-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.backdrop-blur-xl{--tw-backdrop-blur:blur(24px)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}.duration-500{transition-duration:.5s}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}.\[-webkit-mask-image\:linear-gradient\(to_bottom\2c rgba\(255\2c 255\2c 255\2c 1\)_75\%\2c rgba\(255\2c 255\2c 255\2c 0\)\)\]{-webkit-mask-image:linear-gradient(180deg,#fff 75%,#fff0)}.\[-webkit-mask-image\:linear-gradient\(to_top\2c rgba\(255\2c 255\2c 255\2c 1\)_75\%\2c rgba\(255\2c 255\2c 255\2c 0\)\)\]{-webkit-mask-image:linear-gradient(0deg,#fff 75%,#fff0)}.hover\:border-primary-200:hover{--tw-border-opacity:1;border-color:rgb(171 228 214/var(--tw-border-opacity))}.hover\:bg-gray-200:hover{--tw-bg-opacity:1;background-color:rgb(229 231 235/var(--tw-bg-opacity))}.hover\:bg-primary-700:hover{--tw-bg-opacity:1;background-color:rgb(35 98 90/var(--tw-bg-opacity))}.hover\:bg-primary-800:hover{--tw-bg-opacity:1;background-color:rgb(32 79 74/var(--tw-bg-opacity))}.hover\:bg-secondary-700:hover{--tw-bg-opacity:1;background-color:rgb(200 38 13/var(--tw-bg-opacity))}.hover\:bg-opacity-50:hover{--tw-bg-opacity:0.5}.hover\:font-bold:hover{font-weight:700}.hover\:text-primary-400:hover{--tw-text-opacity:1;color:rgb(78 179 161/var(--tw-text-opacity))}.hover\:text-primary-50:hover{--tw-text-opacity:1;color:rgb(243 250 249/var(--tw-text-opacity))}.hover\:text-primary-500:hover{--tw-text-opacity:1;color:rgb(54 156 140/var(--tw-text-opacity))}.hover\:text-primary-700:hover{--tw-text-opacity:1;color:rgb(35 98 90/var(--tw-text-opacity))}.hover\:text-primary-800:hover{--tw-text-opacity:1;color:rgb(32 79 74/var(--tw-text-opacity))}.focus\:border-primary-300:focus{--tw-border-opacity:1;border-color:rgb(122 206 189/var(--tw-border-opacity))}.focus\:border-primary-500:focus{--tw-border-opacity:1;border-color:rgb(54 156 140/var(--tw-border-opacity))}.focus\:bg-opacity-50:focus{--tw-bg-opacity:0.5}.focus\:text-primary-800:focus{--tw-text-opacity:1;color:rgb(32 79 74/var(--tw-text-opacity))}.focus\:outline-none:focus{outline:2px solid #0000;outline-offset:2px}.focus\:ring-\[1px\]:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.focus\:ring-\[1px\]:focus,.focus\:ring-\[3px\]:focus{box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus\:ring-\[3px\]:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.focus\:ring-primary-200:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(171 228 214/var(--tw-ring-opacity))}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:opacity-50:disabled{opacity:.5}.group\/btn:hover .group-hover\/btn\:w-2{width:.5rem}.group\/btn:hover .group-hover\/btn\:w-2\.5{width:.625rem}.group:hover .group-hover\:scale-100{--tw-scale-x:1;--tw-scale-y:1}.group:hover .group-hover\:scale-100,.group:hover .group-hover\:scale-x-100{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group:hover .group-hover\:scale-x-100{--tw-scale-x:1}.group:hover .group-hover\:opacity-100,.group\/btn:hover .group-hover\/btn\:opacity-100{opacity:1}@media (min-width:640px){.sm\:left-14{left:3.5rem}.sm\:w-auto{width:auto}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:px-6{padding-left:1.5rem;padding-right:1.5rem}.sm\:pb-40{padding-bottom:10rem}.sm\:pl-20{padding-left:5rem}.sm\:text-3xl{font-size:1.875rem;line-height:2.25rem}.sm\:text-5xl{font-size:3rem;line-height:1}.sm\:text-sm{font-size:.875rem;line-height:1.25rem}}@media (min-width:768px){.md\:block{display:block}.md\:h-screen{height:100vh}.md\:w-16{width:4rem}.md\:w-20{width:5rem}.md\:w-24{width:6rem}.md\:max-w-2xl{max-width:42rem}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.md\:gap-16{gap:4rem}.md\:px-14{padding-left:3.5rem;padding-right:3.5rem}.md\:py-10{padding-top:2.5rem;padding-bottom:2.5rem}.md\:pb-52{padding-bottom:13rem}.md\:text-3xl{font-size:1.875rem;line-height:2.25rem}.md\:text-4xl{font-size:2.25rem;line-height:2.5rem}.md\:text-xl{font-size:1.25rem;line-height:1.75rem}}@media (min-width:1024px){.lg\:left-20{left:5rem}.lg\:order-last{order:9999}.lg\:col-span-3{grid-column:span 3/span 3}.lg\:col-span-4{grid-column:span 4/span 4}.lg\:-my-11{margin-top:-2.75rem;margin-bottom:-2.75rem}.lg\:-mr-9{margin-right:-2.25rem}.lg\:mb-0{margin-bottom:0}.lg\:mb-16{margin-bottom:4rem}.lg\:mt-0{margin-top:0}.lg\:block{display:block}.lg\:flex{display:flex}.lg\:grid{display:grid}.lg\:max-w-4xl{max-width:56rem}.lg\:max-w-7xl{max-width:80rem}.lg\:max-w-max{max-width:-moz-max-content;max-width:max-content}.lg\:max-w-xl{max-width:36rem}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-7{grid-template-columns:repeat(7,minmax(0,1fr))}.lg\:gap-5{gap:1.25rem}.lg\:p-10{padding:2.5rem}.lg\:px-8{padding-left:2rem;padding-right:2rem}.lg\:py-12{padding-top:3rem;padding-bottom:3rem}.lg\:py-24{padding-top:6rem;padding-bottom:6rem}.lg\:pb-20{padding-bottom:5rem}.lg\:pb-60{padding-bottom:15rem}.lg\:pl-28{padding-left:7rem}.lg\:pt-20{padding-top:5rem}.lg\:pt-40{padding-top:10rem}.lg\:text-4xl{font-size:2.25rem;line-height:2.5rem}.lg\:text-5xl{font-size:3rem;line-height:1}.lg\:text-6xl{font-size:3.75rem;line-height:1}.lg\:text-lg{font-size:1.125rem;line-height:1.75rem}}@media (min-width:1280px){.xl\:col-span-3{grid-column:span 3/span 3}.xl\:mx-auto{margin-left:auto;margin-right:auto}.xl\:flex{display:flex}.xl\:hidden{display:none}.xl\:w-auto{width:auto}.xl\:w-full{width:100%}.xl\:max-w-lg{max-width:32rem}.xl\:max-w-xl{max-width:36rem}.xl\:gap-16{gap:4rem}.xl\:gap-x-16{-moz-column-gap:4rem;column-gap:4rem}.xl\:px-20{padding-left:5rem;padding-right:5rem}.xl\:py-20{padding-top:5rem;padding-bottom:5rem}.xl\:py-32{padding-top:8rem}.xl\:pb-32,.xl\:py-32{padding-bottom:8rem}.xl\:pb-\[16\.5rem\]{padding-bottom:16.5rem}.xl\:text-xl{font-size:1.25rem;line-height:1.75rem}} +/*! tailwindcss v3.4.4 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,::backdrop,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.pointer-events-none{pointer-events:none}.pointer-events-auto{pointer-events:auto}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{inset:0}.inset-1{inset:.25rem}.-inset-x-12{left:-3rem;right:-3rem}.inset-x-1{left:.25rem;right:.25rem}.inset-x-4{left:1rem;right:1rem}.-bottom-0{bottom:0}.-bottom-0\.5{bottom:-.125rem}.-bottom-2{bottom:-.5rem}.-bottom-6{bottom:-1.5rem}.-left-1\/4{left:-25%}.-left-2{left:-.5rem}.-top-1\/4{top:-25%}.-top-4{top:-1rem}.-top-6{top:-1.5rem}.bottom-0{bottom:0}.bottom-10{bottom:2.5rem}.bottom-6{bottom:1.5rem}.left-0{left:0}.left-1\/2{left:50%}.left-8{left:2rem}.right-4{right:1rem}.top-0{top:0}.top-1\/4{top:25%}.z-50{z-index:50}.z-\[-1\]{z-index:-1}.z-\[500\]{z-index:500}.col-span-1{grid-column:span 1/span 1}.col-span-10{grid-column:span 10/span 10}.col-span-2{grid-column:span 2/span 2}.col-span-3{grid-column:span 3/span 3}.col-span-4{grid-column:span 4/span 4}.col-span-7{grid-column:span 7/span 7}.-m-1{margin:-.25rem}.-m-1\.5{margin:-.375rem}.m-auto{margin:auto}.mx-auto{margin-left:auto;margin-right:auto}.my-1{margin-top:.25rem;margin-bottom:.25rem}.my-10{margin-top:2.5rem;margin-bottom:2.5rem}.my-2{margin-top:.5rem;margin-bottom:.5rem}.my-3{margin-top:.75rem;margin-bottom:.75rem}.my-4{margin-top:1rem;margin-bottom:1rem}.my-5{margin-top:1.25rem;margin-bottom:1.25rem}.my-6{margin-top:1.5rem;margin-bottom:1.5rem}.-mb-12{margin-bottom:-3rem}.-mb-6{margin-bottom:-1.5rem}.-ml-2{margin-left:-.5rem}.-mr-2{margin-right:-.5rem}.-mr-2\.5{margin-right:-.625rem}.-mt-1{margin-top:-.25rem}.-mt-24{margin-top:-6rem}.-mt-3{margin-top:-.75rem}.mb-0{margin-bottom:0}.mb-14{margin-bottom:3.5rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-7{margin-bottom:1.75rem}.mb-8{margin-bottom:2rem}.mb-9{margin-bottom:2.25rem}.mb-auto{margin-bottom:auto}.mb-px{margin-bottom:1px}.ml-5{margin-left:1.25rem}.mr-2{margin-right:.5rem}.mr-3{margin-right:.75rem}.mt-1{margin-top:.25rem}.mt-1\.5{margin-top:.375rem}.mt-10{margin-top:2.5rem}.mt-12{margin-top:3rem}.mt-16{margin-top:4rem}.mt-2{margin-top:.5rem}.mt-4{margin-top:1rem}.mt-8{margin-top:2rem}.mt-auto{margin-top:auto}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.size-2{width:.5rem;height:.5rem}.size-2\.5{width:.625rem;height:.625rem}.h-12{height:3rem}.h-2{height:.5rem}.h-2\.5{height:.625rem}.h-24{height:6rem}.h-32{height:8rem}.h-8{height:2rem}.h-96{height:24rem}.h-\[150\%\]{height:150%}.h-full{height:100%}.h-px{height:1px}.max-h-screen{max-height:100vh}.min-h-screen{min-height:100vh}.w-0{width:0}.w-14{width:3.5rem}.w-16{width:4rem}.w-32{width:8rem}.w-60{width:15rem}.w-\[150\%\]{width:150%}.w-fit{width:-moz-fit-content;width:fit-content}.w-full{width:100%}.w-max{width:-moz-max-content;width:max-content}.w-px{width:1px}.min-w-full{min-width:100%}.max-w-5xl{max-width:64rem}.max-w-6xl{max-width:72rem}.max-w-7xl{max-width:80rem}.max-w-\[94\%\]{max-width:94%}.max-w-full{max-width:100%}.max-w-lg{max-width:32rem}.max-w-md{max-width:28rem}.max-w-none{max-width:none}.max-w-sm{max-width:24rem}.max-w-xl{max-width:36rem}.flex-none{flex:none}.shrink-0{flex-shrink:0}.table-auto{table-layout:auto}.border-collapse{border-collapse:collapse}.origin-bottom{transform-origin:bottom}.-translate-x-1\/2{--tw-translate-x:-50%}.-rotate-3,.-translate-x-1\/2{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-rotate-3{--tw-rotate:-3deg}.rotate-3{--tw-rotate:3deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.scale-0{--tw-scale-x:0;--tw-scale-y:0}.scale-0,.scale-x-0{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.scale-x-0{--tw-scale-x:0}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.transform-gpu{transform:translate3d(var(--tw-translate-x),var(--tw-translate-y),0) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes spin{to{transform:rotate(1turn)}}.animate-spin{animation:spin 1s linear infinite}.cursor-pointer{cursor:pointer}.appearance-none{-webkit-appearance:none;-moz-appearance:none;appearance:none}.grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.flex-col{flex-direction:column}.place-items-center{place-items:center}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.justify-items-center{justify-items:center}.gap-0{gap:0}.gap-0\.5{gap:.125rem}.gap-10{gap:2.5rem}.gap-12{gap:3rem}.gap-20{gap:5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-8{gap:2rem}.gap-x-2{-moz-column-gap:.5rem;column-gap:.5rem}.gap-x-8{-moz-column-gap:2rem;column-gap:2rem}.gap-y-16{row-gap:4rem}.gap-y-4{row-gap:1rem}.space-x-1>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.25rem*var(--tw-space-x-reverse));margin-left:calc(.25rem*(1 - var(--tw-space-x-reverse)))}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.5rem*var(--tw-space-x-reverse));margin-left:calc(.5rem*(1 - var(--tw-space-x-reverse)))}.space-x-20>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(5rem*var(--tw-space-x-reverse));margin-left:calc(5rem*(1 - var(--tw-space-x-reverse)))}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem*var(--tw-space-x-reverse));margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)))}.space-x-5>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1.25rem*var(--tw-space-x-reverse));margin-left:calc(1.25rem*(1 - var(--tw-space-x-reverse)))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem*var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.75rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem*var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem*var(--tw-space-y-reverse))}.space-y-5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.25rem*var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem*var(--tw-space-y-reverse))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse:0;border-top-width:calc(1px*(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px*var(--tw-divide-y-reverse))}.divide-gray-200>:not([hidden])~:not([hidden]){--tw-divide-opacity:1;border-color:rgb(229 231 235/var(--tw-divide-opacity))}.justify-self-start{justify-self:start}.justify-self-end{justify-self:end}.overflow-hidden{overflow:hidden}.overflow-visible{overflow:visible}.overflow-x-auto{overflow-x:auto}.whitespace-normal{white-space:normal}.whitespace-nowrap{white-space:nowrap}.text-wrap{text-wrap:wrap}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-3xl{border-radius:1.5rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-xl{border-radius:.75rem}.rounded-l-\[20px\]{border-top-left-radius:20px;border-bottom-left-radius:20px}.rounded-r-\[20px\]{border-top-right-radius:20px;border-bottom-right-radius:20px}.rounded-r-\[8px\]{border-top-right-radius:8px;border-bottom-right-radius:8px}.rounded-t-full{border-top-left-radius:9999px;border-top-right-radius:9999px}.border{border-width:1px}.border-l-0{border-left-width:0}.border-r-0{border-right-width:0}.border-t-0{border-top-width:0}.border-none{border-style:none}.border-\[\#D7DFE9\]{--tw-border-opacity:1;border-color:rgb(215 223 233/var(--tw-border-opacity))}.border-primary-200{--tw-border-opacity:1;border-color:rgb(171 228 214/var(--tw-border-opacity))}.border-primary-400{--tw-border-opacity:1;border-color:rgb(78 179 161/var(--tw-border-opacity))}.border-primary-500{--tw-border-opacity:1;border-color:rgb(54 156 140/var(--tw-border-opacity))}.border-primary-600{--tw-border-opacity:1;border-color:rgb(40 121 110/var(--tw-border-opacity))}.border-primary-700{--tw-border-opacity:1;border-color:rgb(35 98 90/var(--tw-border-opacity))}.border-y-secondary-300{--tw-border-opacity:1;border-top-color:rgb(255 170 157/var(--tw-border-opacity));border-bottom-color:rgb(255 170 157/var(--tw-border-opacity))}.bg-gray-100{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity))}.bg-gray-500\/25{background-color:#6b728040}.bg-primary-100{--tw-bg-opacity:1;background-color:rgb(213 242 235/var(--tw-bg-opacity))}.bg-primary-200{--tw-bg-opacity:1;background-color:rgb(171 228 214/var(--tw-bg-opacity))}.bg-primary-300{--tw-bg-opacity:1;background-color:rgb(122 206 189/var(--tw-bg-opacity))}.bg-primary-50{--tw-bg-opacity:1;background-color:rgb(243 250 249/var(--tw-bg-opacity))}.bg-primary-500{--tw-bg-opacity:1;background-color:rgb(54 156 140/var(--tw-bg-opacity))}.bg-primary-800{--tw-bg-opacity:1;background-color:rgb(32 79 74/var(--tw-bg-opacity))}.bg-primary-950{--tw-bg-opacity:1;background-color:rgb(13 38 37/var(--tw-bg-opacity))}.bg-secondary-300{--tw-bg-opacity:1;background-color:rgb(255 170 157/var(--tw-bg-opacity))}.bg-secondary-500{--tw-bg-opacity:1;background-color:rgb(255 78 51/var(--tw-bg-opacity))}.bg-secondary-700{--tw-bg-opacity:1;background-color:rgb(200 38 13/var(--tw-bg-opacity))}.bg-transparent{background-color:initial}.bg-opacity-0{--tw-bg-opacity:0}.bg-gradient-to-b{background-image:linear-gradient(to bottom,var(--tw-gradient-stops))}.bg-gradient-to-br{background-image:linear-gradient(to bottom right,var(--tw-gradient-stops))}.bg-gradient-to-r{background-image:linear-gradient(to right,var(--tw-gradient-stops))}.bg-gradient-to-t{background-image:linear-gradient(to top,var(--tw-gradient-stops))}.from-gray-100{--tw-gradient-from:#f3f4f6 var(--tw-gradient-from-position);--tw-gradient-to:#f3f4f600 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-primary-900\/50{--tw-gradient-from:#1f423e80 var(--tw-gradient-from-position);--tw-gradient-to:#1f423e00 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-primary-950{--tw-gradient-from:#0d2625 var(--tw-gradient-from-position);--tw-gradient-to:#0d262500 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-transparent{--tw-gradient-from:#0000 var(--tw-gradient-from-position);--tw-gradient-to:#0000 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-10\%{--tw-gradient-from-position:10%}.via-primary-900{--tw-gradient-to:#1f423e00 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),#1f423e var(--tw-gradient-via-position),var(--tw-gradient-to)}.via-primary-900\/50{--tw-gradient-to:#1f423e00 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),#1f423e80 var(--tw-gradient-via-position),var(--tw-gradient-to)}.via-10\%{--tw-gradient-via-position:10%}.to-primary-950{--tw-gradient-to:#0d2625 var(--tw-gradient-to-position)}.to-primary-950\/50{--tw-gradient-to:#0d262580 var(--tw-gradient-to-position)}.to-transparent{--tw-gradient-to:#0000 var(--tw-gradient-to-position)}.to-90\%{--tw-gradient-to-position:90%}.bg-\[length\:100px_auto\]{background-size:100px auto}.bg-cover{background-size:cover}.bg-clip-padding{background-clip:padding-box}.bg-center{background-position:50%}.fill-primary-400{fill:#4eb3a1}.object-contain{-o-object-fit:contain;object-fit:contain}.object-cover{-o-object-fit:cover;object-fit:cover}.object-bottom{-o-object-position:bottom;object-position:bottom}.p-0{padding:0}.p-1{padding:.25rem}.p-1\.5{padding:.375rem}.p-10{padding:2.5rem}.p-16{padding:4rem}.p-2{padding:.5rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.px-10{padding-left:2.5rem;padding-right:2.5rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-40{padding-left:10rem;padding-right:10rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-0{padding-top:0;padding-bottom:0}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-10{padding-top:2.5rem;padding-bottom:2.5rem}.py-16{padding-top:4rem;padding-bottom:4rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-24{padding-top:6rem;padding-bottom:6rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-5{padding-top:1.25rem;padding-bottom:1.25rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.py-8{padding-top:2rem;padding-bottom:2rem}.pb-12{padding-bottom:3rem}.pb-16{padding-bottom:4rem}.pb-3{padding-bottom:.75rem}.pb-32{padding-bottom:8rem}.pb-8{padding-bottom:2rem}.pl-3{padding-left:.75rem}.pl-4{padding-left:1rem}.pr-3{padding-right:.75rem}.pr-7{padding-right:1.75rem}.pr-\[10px\]{padding-right:10px}.pt-12{padding-top:3rem}.pt-24{padding-top:6rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.text-start{text-align:start}.align-middle{vertical-align:middle}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-5xl{font-size:3rem;line-height:1}.text-7xl{font-size:4.5rem;line-height:1}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-\[450\]{font-weight:450}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.lowercase{text-transform:lowercase}.leading-6{line-height:1.5rem}.leading-none{line-height:1}.tracking-tight{letter-spacing:-.025em}.tracking-wider{letter-spacing:.05em}.text-gray-100{--tw-text-opacity:1;color:rgb(243 244 246/var(--tw-text-opacity))}.text-gray-200{--tw-text-opacity:1;color:rgb(229 231 235/var(--tw-text-opacity))}.text-gray-50{--tw-text-opacity:1;color:rgb(249 250 251/var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.text-gray-600{--tw-text-opacity:1;color:rgb(75 85 99/var(--tw-text-opacity))}.text-gray-700{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity))}.text-gray-800{--tw-text-opacity:1;color:rgb(31 41 55/var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity:1;color:rgb(17 24 39/var(--tw-text-opacity))}.text-primary-300{--tw-text-opacity:1;color:rgb(122 206 189/var(--tw-text-opacity))}.text-primary-400{--tw-text-opacity:1;color:rgb(78 179 161/var(--tw-text-opacity))}.text-primary-50{--tw-text-opacity:1;color:rgb(243 250 249/var(--tw-text-opacity))}.text-primary-500{--tw-text-opacity:1;color:rgb(54 156 140/var(--tw-text-opacity))}.text-primary-600{--tw-text-opacity:1;color:rgb(40 121 110/var(--tw-text-opacity))}.text-primary-800{--tw-text-opacity:1;color:rgb(32 79 74/var(--tw-text-opacity))}.text-primary-900{--tw-text-opacity:1;color:rgb(31 66 62/var(--tw-text-opacity))}.text-primary-950{--tw-text-opacity:1;color:rgb(13 38 37/var(--tw-text-opacity))}.text-secondary-300{--tw-text-opacity:1;color:rgb(255 170 157/var(--tw-text-opacity))}.text-secondary-50{--tw-text-opacity:1;color:rgb(255 243 241/var(--tw-text-opacity))}.text-secondary-500{--tw-text-opacity:1;color:rgb(255 78 51/var(--tw-text-opacity))}.opacity-0{opacity:0}.opacity-50{opacity:.5}.shadow{--tw-shadow:0 1px 3px 0 #0000001a,0 1px 2px -1px #0000001a;--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color)}.shadow,.shadow-lg{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a;--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}.shadow-md{--tw-shadow:0 4px 6px -1px #0000001a,0 2px 4px -2px #0000001a;--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color)}.shadow-md,.shadow-sm{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 2px 0 #0000000d;--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color)}.shadow-gray-800\/5{--tw-shadow-color:#1f29370d;--tw-shadow:var(--tw-shadow-colored)}.outline-0{outline-width:0}.ring-1{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.ring-inset{--tw-ring-inset:inset}.ring-gray-800\/\[\.075\]{--tw-ring-color:rgba(31,41,55,.075)}.ring-primary-100{--tw-ring-opacity:1;--tw-ring-color:rgb(213 242 235/var(--tw-ring-opacity))}.ring-primary-200{--tw-ring-opacity:1;--tw-ring-color:rgb(171 228 214/var(--tw-ring-opacity))}.blur{--tw-blur:blur(8px)}.blur,.blur-3xl{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.blur-3xl{--tw-blur:blur(64px)}.brightness-0{--tw-brightness:brightness(0)}.brightness-0,.drop-shadow{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.drop-shadow{--tw-drop-shadow:drop-shadow(0 1px 2px #0000001a) drop-shadow(0 1px 1px #0000000f)}.drop-shadow-sm{--tw-drop-shadow:drop-shadow(0 1px 1px #0000000d)}.drop-shadow-sm,.invert{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.invert{--tw-invert:invert(100%)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur-sm{--tw-backdrop-blur:blur(4px)}.backdrop-blur-sm,.backdrop-blur-xl{-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.backdrop-blur-xl{--tw-backdrop-blur:blur(24px)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}.duration-500{transition-duration:.5s}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}.\[-webkit-mask-image\:linear-gradient\(to_bottom\2c rgba\(255\2c 255\2c 255\2c 1\)_75\%\2c rgba\(255\2c 255\2c 255\2c 0\)\)\]{-webkit-mask-image:linear-gradient(180deg,#fff 75%,#fff0)}.\[-webkit-mask-image\:linear-gradient\(to_top\2c rgba\(255\2c 255\2c 255\2c 1\)_75\%\2c rgba\(255\2c 255\2c 255\2c 0\)\)\]{-webkit-mask-image:linear-gradient(0deg,#fff 75%,#fff0)}.hover\:border-primary-200:hover{--tw-border-opacity:1;border-color:rgb(171 228 214/var(--tw-border-opacity))}.hover\:bg-gray-200:hover{--tw-bg-opacity:1;background-color:rgb(229 231 235/var(--tw-bg-opacity))}.hover\:bg-primary-700:hover{--tw-bg-opacity:1;background-color:rgb(35 98 90/var(--tw-bg-opacity))}.hover\:bg-primary-800:hover{--tw-bg-opacity:1;background-color:rgb(32 79 74/var(--tw-bg-opacity))}.hover\:bg-secondary-700:hover{--tw-bg-opacity:1;background-color:rgb(200 38 13/var(--tw-bg-opacity))}.hover\:bg-opacity-50:hover{--tw-bg-opacity:0.5}.hover\:font-bold:hover{font-weight:700}.hover\:text-primary-400:hover{--tw-text-opacity:1;color:rgb(78 179 161/var(--tw-text-opacity))}.hover\:text-primary-50:hover{--tw-text-opacity:1;color:rgb(243 250 249/var(--tw-text-opacity))}.hover\:text-primary-500:hover{--tw-text-opacity:1;color:rgb(54 156 140/var(--tw-text-opacity))}.hover\:text-primary-700:hover{--tw-text-opacity:1;color:rgb(35 98 90/var(--tw-text-opacity))}.hover\:text-primary-800:hover{--tw-text-opacity:1;color:rgb(32 79 74/var(--tw-text-opacity))}.focus\:border-primary-300:focus{--tw-border-opacity:1;border-color:rgb(122 206 189/var(--tw-border-opacity))}.focus\:border-primary-500:focus{--tw-border-opacity:1;border-color:rgb(54 156 140/var(--tw-border-opacity))}.focus\:bg-opacity-50:focus{--tw-bg-opacity:0.5}.focus\:text-primary-800:focus{--tw-text-opacity:1;color:rgb(32 79 74/var(--tw-text-opacity))}.focus\:outline-none:focus{outline:2px solid #0000;outline-offset:2px}.focus\:ring-\[1px\]:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.focus\:ring-\[1px\]:focus,.focus\:ring-\[3px\]:focus{box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus\:ring-\[3px\]:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.focus\:ring-primary-200:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(171 228 214/var(--tw-ring-opacity))}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:opacity-50:disabled{opacity:.5}.group\/btn:hover .group-hover\/btn\:w-2{width:.5rem}.group\/btn:hover .group-hover\/btn\:w-2\.5{width:.625rem}.group:hover .group-hover\:scale-100{--tw-scale-x:1;--tw-scale-y:1}.group:hover .group-hover\:scale-100,.group:hover .group-hover\:scale-x-100{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group:hover .group-hover\:scale-x-100{--tw-scale-x:1}.group:hover .group-hover\:opacity-100,.group\/btn:hover .group-hover\/btn\:opacity-100{opacity:1}@media (min-width:640px){.sm\:left-14{left:3.5rem}.sm\:w-auto{width:auto}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:px-6{padding-left:1.5rem;padding-right:1.5rem}.sm\:pb-40{padding-bottom:10rem}.sm\:pl-20{padding-left:5rem}.sm\:text-3xl{font-size:1.875rem;line-height:2.25rem}.sm\:text-5xl{font-size:3rem;line-height:1}.sm\:text-sm{font-size:.875rem;line-height:1.25rem}}@media (min-width:768px){.md\:block{display:block}.md\:h-screen{height:100vh}.md\:w-16{width:4rem}.md\:w-20{width:5rem}.md\:w-24{width:6rem}.md\:max-w-2xl{max-width:42rem}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.md\:gap-16{gap:4rem}.md\:px-14{padding-left:3.5rem;padding-right:3.5rem}.md\:py-10{padding-top:2.5rem;padding-bottom:2.5rem}.md\:pb-52{padding-bottom:13rem}.md\:text-3xl{font-size:1.875rem;line-height:2.25rem}.md\:text-4xl{font-size:2.25rem;line-height:2.5rem}.md\:text-xl{font-size:1.25rem;line-height:1.75rem}}@media (min-width:1024px){.lg\:left-20{left:5rem}.lg\:order-last{order:9999}.lg\:col-span-3{grid-column:span 3/span 3}.lg\:col-span-4{grid-column:span 4/span 4}.lg\:-my-11{margin-top:-2.75rem;margin-bottom:-2.75rem}.lg\:-mr-9{margin-right:-2.25rem}.lg\:mb-0{margin-bottom:0}.lg\:mb-16{margin-bottom:4rem}.lg\:mt-0{margin-top:0}.lg\:block{display:block}.lg\:flex{display:flex}.lg\:grid{display:grid}.lg\:max-w-4xl{max-width:56rem}.lg\:max-w-7xl{max-width:80rem}.lg\:max-w-max{max-width:-moz-max-content;max-width:max-content}.lg\:max-w-xl{max-width:36rem}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-7{grid-template-columns:repeat(7,minmax(0,1fr))}.lg\:gap-5{gap:1.25rem}.lg\:p-10{padding:2.5rem}.lg\:px-8{padding-left:2rem;padding-right:2rem}.lg\:py-12{padding-top:3rem;padding-bottom:3rem}.lg\:py-24{padding-top:6rem;padding-bottom:6rem}.lg\:pb-20{padding-bottom:5rem}.lg\:pb-60{padding-bottom:15rem}.lg\:pl-28{padding-left:7rem}.lg\:pt-20{padding-top:5rem}.lg\:pt-40{padding-top:10rem}.lg\:text-4xl{font-size:2.25rem;line-height:2.5rem}.lg\:text-5xl{font-size:3rem;line-height:1}.lg\:text-6xl{font-size:3.75rem;line-height:1}.lg\:text-lg{font-size:1.125rem;line-height:1.75rem}}@media (min-width:1280px){.xl\:col-span-3{grid-column:span 3/span 3}.xl\:mx-auto{margin-left:auto;margin-right:auto}.xl\:flex{display:flex}.xl\:hidden{display:none}.xl\:w-auto{width:auto}.xl\:w-full{width:100%}.xl\:max-w-lg{max-width:32rem}.xl\:max-w-xl{max-width:36rem}.xl\:gap-16{gap:4rem}.xl\:gap-x-16{-moz-column-gap:4rem;column-gap:4rem}.xl\:px-20{padding-left:5rem;padding-right:5rem}.xl\:py-20{padding-top:5rem;padding-bottom:5rem}.xl\:py-32{padding-top:8rem}.xl\:pb-32,.xl\:py-32{padding-bottom:8rem}.xl\:pb-\[16\.5rem\]{padding-bottom:16.5rem}.xl\:text-xl{font-size:1.25rem;line-height:1.75rem}} From 6c7f4287fc100f6a1c24f87a9ac4bea63f6acf38 Mon Sep 17 00:00:00 2001 From: PizieDust Date: Mon, 9 Sep 2024 07:12:50 +0200 Subject: [PATCH 14/21] single user view --- assets/style.css | 2 +- unikernel.ml | 50 ++++++++++++++++++++++++ user_single.ml | 100 +++++++++++++++++++++++++++++++++++++++++++++++ users_index.ml | 49 +++++++---------------- 4 files changed, 166 insertions(+), 35 deletions(-) create mode 100644 user_single.ml diff --git a/assets/style.css b/assets/style.css index d46740b6..a75f9d99 100755 --- a/assets/style.css +++ b/assets/style.css @@ -1 +1 @@ -/*! tailwindcss v3.4.4 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,::backdrop,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.pointer-events-none{pointer-events:none}.pointer-events-auto{pointer-events:auto}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{inset:0}.inset-1{inset:.25rem}.-inset-x-12{left:-3rem;right:-3rem}.inset-x-1{left:.25rem;right:.25rem}.inset-x-4{left:1rem;right:1rem}.-bottom-0{bottom:0}.-bottom-0\.5{bottom:-.125rem}.-bottom-2{bottom:-.5rem}.-bottom-6{bottom:-1.5rem}.-left-1\/4{left:-25%}.-left-2{left:-.5rem}.-top-1\/4{top:-25%}.-top-4{top:-1rem}.-top-6{top:-1.5rem}.bottom-0{bottom:0}.bottom-10{bottom:2.5rem}.bottom-6{bottom:1.5rem}.left-0{left:0}.left-1\/2{left:50%}.left-8{left:2rem}.right-4{right:1rem}.top-0{top:0}.top-1\/4{top:25%}.z-50{z-index:50}.z-\[-1\]{z-index:-1}.z-\[500\]{z-index:500}.col-span-1{grid-column:span 1/span 1}.col-span-10{grid-column:span 10/span 10}.col-span-2{grid-column:span 2/span 2}.col-span-3{grid-column:span 3/span 3}.col-span-4{grid-column:span 4/span 4}.col-span-7{grid-column:span 7/span 7}.-m-1{margin:-.25rem}.-m-1\.5{margin:-.375rem}.m-auto{margin:auto}.mx-auto{margin-left:auto;margin-right:auto}.my-1{margin-top:.25rem;margin-bottom:.25rem}.my-10{margin-top:2.5rem;margin-bottom:2.5rem}.my-2{margin-top:.5rem;margin-bottom:.5rem}.my-3{margin-top:.75rem;margin-bottom:.75rem}.my-4{margin-top:1rem;margin-bottom:1rem}.my-5{margin-top:1.25rem;margin-bottom:1.25rem}.my-6{margin-top:1.5rem;margin-bottom:1.5rem}.-mb-12{margin-bottom:-3rem}.-mb-6{margin-bottom:-1.5rem}.-ml-2{margin-left:-.5rem}.-mr-2{margin-right:-.5rem}.-mr-2\.5{margin-right:-.625rem}.-mt-1{margin-top:-.25rem}.-mt-24{margin-top:-6rem}.-mt-3{margin-top:-.75rem}.mb-0{margin-bottom:0}.mb-14{margin-bottom:3.5rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-7{margin-bottom:1.75rem}.mb-8{margin-bottom:2rem}.mb-9{margin-bottom:2.25rem}.mb-auto{margin-bottom:auto}.mb-px{margin-bottom:1px}.ml-5{margin-left:1.25rem}.mr-2{margin-right:.5rem}.mr-3{margin-right:.75rem}.mt-1{margin-top:.25rem}.mt-1\.5{margin-top:.375rem}.mt-10{margin-top:2.5rem}.mt-12{margin-top:3rem}.mt-16{margin-top:4rem}.mt-2{margin-top:.5rem}.mt-4{margin-top:1rem}.mt-8{margin-top:2rem}.mt-auto{margin-top:auto}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.size-2{width:.5rem;height:.5rem}.size-2\.5{width:.625rem;height:.625rem}.h-12{height:3rem}.h-2{height:.5rem}.h-2\.5{height:.625rem}.h-24{height:6rem}.h-32{height:8rem}.h-8{height:2rem}.h-96{height:24rem}.h-\[150\%\]{height:150%}.h-full{height:100%}.h-px{height:1px}.max-h-screen{max-height:100vh}.min-h-screen{min-height:100vh}.w-0{width:0}.w-14{width:3.5rem}.w-16{width:4rem}.w-32{width:8rem}.w-60{width:15rem}.w-\[150\%\]{width:150%}.w-fit{width:-moz-fit-content;width:fit-content}.w-full{width:100%}.w-max{width:-moz-max-content;width:max-content}.w-px{width:1px}.min-w-full{min-width:100%}.max-w-5xl{max-width:64rem}.max-w-6xl{max-width:72rem}.max-w-7xl{max-width:80rem}.max-w-\[94\%\]{max-width:94%}.max-w-full{max-width:100%}.max-w-lg{max-width:32rem}.max-w-md{max-width:28rem}.max-w-none{max-width:none}.max-w-sm{max-width:24rem}.max-w-xl{max-width:36rem}.flex-none{flex:none}.shrink-0{flex-shrink:0}.table-auto{table-layout:auto}.border-collapse{border-collapse:collapse}.origin-bottom{transform-origin:bottom}.-translate-x-1\/2{--tw-translate-x:-50%}.-rotate-3,.-translate-x-1\/2{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-rotate-3{--tw-rotate:-3deg}.rotate-3{--tw-rotate:3deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.scale-0{--tw-scale-x:0;--tw-scale-y:0}.scale-0,.scale-x-0{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.scale-x-0{--tw-scale-x:0}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.transform-gpu{transform:translate3d(var(--tw-translate-x),var(--tw-translate-y),0) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes spin{to{transform:rotate(1turn)}}.animate-spin{animation:spin 1s linear infinite}.cursor-pointer{cursor:pointer}.appearance-none{-webkit-appearance:none;-moz-appearance:none;appearance:none}.grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.flex-col{flex-direction:column}.place-items-center{place-items:center}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.justify-items-center{justify-items:center}.gap-0{gap:0}.gap-0\.5{gap:.125rem}.gap-10{gap:2.5rem}.gap-12{gap:3rem}.gap-20{gap:5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-8{gap:2rem}.gap-x-2{-moz-column-gap:.5rem;column-gap:.5rem}.gap-x-8{-moz-column-gap:2rem;column-gap:2rem}.gap-y-16{row-gap:4rem}.gap-y-4{row-gap:1rem}.space-x-1>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.25rem*var(--tw-space-x-reverse));margin-left:calc(.25rem*(1 - var(--tw-space-x-reverse)))}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.5rem*var(--tw-space-x-reverse));margin-left:calc(.5rem*(1 - var(--tw-space-x-reverse)))}.space-x-20>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(5rem*var(--tw-space-x-reverse));margin-left:calc(5rem*(1 - var(--tw-space-x-reverse)))}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem*var(--tw-space-x-reverse));margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)))}.space-x-5>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1.25rem*var(--tw-space-x-reverse));margin-left:calc(1.25rem*(1 - var(--tw-space-x-reverse)))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem*var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.75rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem*var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem*var(--tw-space-y-reverse))}.space-y-5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.25rem*var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem*var(--tw-space-y-reverse))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse:0;border-top-width:calc(1px*(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px*var(--tw-divide-y-reverse))}.divide-gray-200>:not([hidden])~:not([hidden]){--tw-divide-opacity:1;border-color:rgb(229 231 235/var(--tw-divide-opacity))}.justify-self-start{justify-self:start}.justify-self-end{justify-self:end}.overflow-hidden{overflow:hidden}.overflow-visible{overflow:visible}.overflow-x-auto{overflow-x:auto}.whitespace-normal{white-space:normal}.whitespace-nowrap{white-space:nowrap}.text-wrap{text-wrap:wrap}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-3xl{border-radius:1.5rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-xl{border-radius:.75rem}.rounded-l-\[20px\]{border-top-left-radius:20px;border-bottom-left-radius:20px}.rounded-r-\[20px\]{border-top-right-radius:20px;border-bottom-right-radius:20px}.rounded-r-\[8px\]{border-top-right-radius:8px;border-bottom-right-radius:8px}.rounded-t-full{border-top-left-radius:9999px;border-top-right-radius:9999px}.border{border-width:1px}.border-l-0{border-left-width:0}.border-r-0{border-right-width:0}.border-t-0{border-top-width:0}.border-none{border-style:none}.border-\[\#D7DFE9\]{--tw-border-opacity:1;border-color:rgb(215 223 233/var(--tw-border-opacity))}.border-primary-200{--tw-border-opacity:1;border-color:rgb(171 228 214/var(--tw-border-opacity))}.border-primary-400{--tw-border-opacity:1;border-color:rgb(78 179 161/var(--tw-border-opacity))}.border-primary-500{--tw-border-opacity:1;border-color:rgb(54 156 140/var(--tw-border-opacity))}.border-primary-600{--tw-border-opacity:1;border-color:rgb(40 121 110/var(--tw-border-opacity))}.border-primary-700{--tw-border-opacity:1;border-color:rgb(35 98 90/var(--tw-border-opacity))}.border-y-secondary-300{--tw-border-opacity:1;border-top-color:rgb(255 170 157/var(--tw-border-opacity));border-bottom-color:rgb(255 170 157/var(--tw-border-opacity))}.bg-gray-100{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity))}.bg-gray-500\/25{background-color:#6b728040}.bg-primary-100{--tw-bg-opacity:1;background-color:rgb(213 242 235/var(--tw-bg-opacity))}.bg-primary-200{--tw-bg-opacity:1;background-color:rgb(171 228 214/var(--tw-bg-opacity))}.bg-primary-300{--tw-bg-opacity:1;background-color:rgb(122 206 189/var(--tw-bg-opacity))}.bg-primary-50{--tw-bg-opacity:1;background-color:rgb(243 250 249/var(--tw-bg-opacity))}.bg-primary-500{--tw-bg-opacity:1;background-color:rgb(54 156 140/var(--tw-bg-opacity))}.bg-primary-800{--tw-bg-opacity:1;background-color:rgb(32 79 74/var(--tw-bg-opacity))}.bg-primary-950{--tw-bg-opacity:1;background-color:rgb(13 38 37/var(--tw-bg-opacity))}.bg-secondary-300{--tw-bg-opacity:1;background-color:rgb(255 170 157/var(--tw-bg-opacity))}.bg-secondary-500{--tw-bg-opacity:1;background-color:rgb(255 78 51/var(--tw-bg-opacity))}.bg-secondary-700{--tw-bg-opacity:1;background-color:rgb(200 38 13/var(--tw-bg-opacity))}.bg-transparent{background-color:initial}.bg-opacity-0{--tw-bg-opacity:0}.bg-gradient-to-b{background-image:linear-gradient(to bottom,var(--tw-gradient-stops))}.bg-gradient-to-br{background-image:linear-gradient(to bottom right,var(--tw-gradient-stops))}.bg-gradient-to-r{background-image:linear-gradient(to right,var(--tw-gradient-stops))}.bg-gradient-to-t{background-image:linear-gradient(to top,var(--tw-gradient-stops))}.from-gray-100{--tw-gradient-from:#f3f4f6 var(--tw-gradient-from-position);--tw-gradient-to:#f3f4f600 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-primary-900\/50{--tw-gradient-from:#1f423e80 var(--tw-gradient-from-position);--tw-gradient-to:#1f423e00 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-primary-950{--tw-gradient-from:#0d2625 var(--tw-gradient-from-position);--tw-gradient-to:#0d262500 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-transparent{--tw-gradient-from:#0000 var(--tw-gradient-from-position);--tw-gradient-to:#0000 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-10\%{--tw-gradient-from-position:10%}.via-primary-900{--tw-gradient-to:#1f423e00 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),#1f423e var(--tw-gradient-via-position),var(--tw-gradient-to)}.via-primary-900\/50{--tw-gradient-to:#1f423e00 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),#1f423e80 var(--tw-gradient-via-position),var(--tw-gradient-to)}.via-10\%{--tw-gradient-via-position:10%}.to-primary-950{--tw-gradient-to:#0d2625 var(--tw-gradient-to-position)}.to-primary-950\/50{--tw-gradient-to:#0d262580 var(--tw-gradient-to-position)}.to-transparent{--tw-gradient-to:#0000 var(--tw-gradient-to-position)}.to-90\%{--tw-gradient-to-position:90%}.bg-\[length\:100px_auto\]{background-size:100px auto}.bg-cover{background-size:cover}.bg-clip-padding{background-clip:padding-box}.bg-center{background-position:50%}.fill-primary-400{fill:#4eb3a1}.object-contain{-o-object-fit:contain;object-fit:contain}.object-cover{-o-object-fit:cover;object-fit:cover}.object-bottom{-o-object-position:bottom;object-position:bottom}.p-0{padding:0}.p-1{padding:.25rem}.p-1\.5{padding:.375rem}.p-10{padding:2.5rem}.p-16{padding:4rem}.p-2{padding:.5rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.px-10{padding-left:2.5rem;padding-right:2.5rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-40{padding-left:10rem;padding-right:10rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-0{padding-top:0;padding-bottom:0}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-10{padding-top:2.5rem;padding-bottom:2.5rem}.py-16{padding-top:4rem;padding-bottom:4rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-24{padding-top:6rem;padding-bottom:6rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-5{padding-top:1.25rem;padding-bottom:1.25rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.py-8{padding-top:2rem;padding-bottom:2rem}.pb-12{padding-bottom:3rem}.pb-16{padding-bottom:4rem}.pb-3{padding-bottom:.75rem}.pb-32{padding-bottom:8rem}.pb-8{padding-bottom:2rem}.pl-3{padding-left:.75rem}.pl-4{padding-left:1rem}.pr-3{padding-right:.75rem}.pr-7{padding-right:1.75rem}.pr-\[10px\]{padding-right:10px}.pt-12{padding-top:3rem}.pt-24{padding-top:6rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.text-start{text-align:start}.align-middle{vertical-align:middle}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-5xl{font-size:3rem;line-height:1}.text-7xl{font-size:4.5rem;line-height:1}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-\[450\]{font-weight:450}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.lowercase{text-transform:lowercase}.leading-6{line-height:1.5rem}.leading-none{line-height:1}.tracking-tight{letter-spacing:-.025em}.tracking-wider{letter-spacing:.05em}.text-gray-100{--tw-text-opacity:1;color:rgb(243 244 246/var(--tw-text-opacity))}.text-gray-200{--tw-text-opacity:1;color:rgb(229 231 235/var(--tw-text-opacity))}.text-gray-50{--tw-text-opacity:1;color:rgb(249 250 251/var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.text-gray-600{--tw-text-opacity:1;color:rgb(75 85 99/var(--tw-text-opacity))}.text-gray-700{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity))}.text-gray-800{--tw-text-opacity:1;color:rgb(31 41 55/var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity:1;color:rgb(17 24 39/var(--tw-text-opacity))}.text-primary-300{--tw-text-opacity:1;color:rgb(122 206 189/var(--tw-text-opacity))}.text-primary-400{--tw-text-opacity:1;color:rgb(78 179 161/var(--tw-text-opacity))}.text-primary-50{--tw-text-opacity:1;color:rgb(243 250 249/var(--tw-text-opacity))}.text-primary-500{--tw-text-opacity:1;color:rgb(54 156 140/var(--tw-text-opacity))}.text-primary-600{--tw-text-opacity:1;color:rgb(40 121 110/var(--tw-text-opacity))}.text-primary-800{--tw-text-opacity:1;color:rgb(32 79 74/var(--tw-text-opacity))}.text-primary-900{--tw-text-opacity:1;color:rgb(31 66 62/var(--tw-text-opacity))}.text-primary-950{--tw-text-opacity:1;color:rgb(13 38 37/var(--tw-text-opacity))}.text-secondary-300{--tw-text-opacity:1;color:rgb(255 170 157/var(--tw-text-opacity))}.text-secondary-50{--tw-text-opacity:1;color:rgb(255 243 241/var(--tw-text-opacity))}.text-secondary-500{--tw-text-opacity:1;color:rgb(255 78 51/var(--tw-text-opacity))}.opacity-0{opacity:0}.opacity-50{opacity:.5}.shadow{--tw-shadow:0 1px 3px 0 #0000001a,0 1px 2px -1px #0000001a;--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color)}.shadow,.shadow-lg{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a;--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}.shadow-md{--tw-shadow:0 4px 6px -1px #0000001a,0 2px 4px -2px #0000001a;--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color)}.shadow-md,.shadow-sm{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 2px 0 #0000000d;--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color)}.shadow-gray-800\/5{--tw-shadow-color:#1f29370d;--tw-shadow:var(--tw-shadow-colored)}.outline-0{outline-width:0}.ring-1{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.ring-inset{--tw-ring-inset:inset}.ring-gray-800\/\[\.075\]{--tw-ring-color:rgba(31,41,55,.075)}.ring-primary-100{--tw-ring-opacity:1;--tw-ring-color:rgb(213 242 235/var(--tw-ring-opacity))}.ring-primary-200{--tw-ring-opacity:1;--tw-ring-color:rgb(171 228 214/var(--tw-ring-opacity))}.blur{--tw-blur:blur(8px)}.blur,.blur-3xl{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.blur-3xl{--tw-blur:blur(64px)}.brightness-0{--tw-brightness:brightness(0)}.brightness-0,.drop-shadow{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.drop-shadow{--tw-drop-shadow:drop-shadow(0 1px 2px #0000001a) drop-shadow(0 1px 1px #0000000f)}.drop-shadow-sm{--tw-drop-shadow:drop-shadow(0 1px 1px #0000000d)}.drop-shadow-sm,.invert{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.invert{--tw-invert:invert(100%)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur-sm{--tw-backdrop-blur:blur(4px)}.backdrop-blur-sm,.backdrop-blur-xl{-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.backdrop-blur-xl{--tw-backdrop-blur:blur(24px)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}.duration-500{transition-duration:.5s}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}.\[-webkit-mask-image\:linear-gradient\(to_bottom\2c rgba\(255\2c 255\2c 255\2c 1\)_75\%\2c rgba\(255\2c 255\2c 255\2c 0\)\)\]{-webkit-mask-image:linear-gradient(180deg,#fff 75%,#fff0)}.\[-webkit-mask-image\:linear-gradient\(to_top\2c rgba\(255\2c 255\2c 255\2c 1\)_75\%\2c rgba\(255\2c 255\2c 255\2c 0\)\)\]{-webkit-mask-image:linear-gradient(0deg,#fff 75%,#fff0)}.hover\:border-primary-200:hover{--tw-border-opacity:1;border-color:rgb(171 228 214/var(--tw-border-opacity))}.hover\:bg-gray-200:hover{--tw-bg-opacity:1;background-color:rgb(229 231 235/var(--tw-bg-opacity))}.hover\:bg-primary-700:hover{--tw-bg-opacity:1;background-color:rgb(35 98 90/var(--tw-bg-opacity))}.hover\:bg-primary-800:hover{--tw-bg-opacity:1;background-color:rgb(32 79 74/var(--tw-bg-opacity))}.hover\:bg-secondary-700:hover{--tw-bg-opacity:1;background-color:rgb(200 38 13/var(--tw-bg-opacity))}.hover\:bg-opacity-50:hover{--tw-bg-opacity:0.5}.hover\:font-bold:hover{font-weight:700}.hover\:text-primary-400:hover{--tw-text-opacity:1;color:rgb(78 179 161/var(--tw-text-opacity))}.hover\:text-primary-50:hover{--tw-text-opacity:1;color:rgb(243 250 249/var(--tw-text-opacity))}.hover\:text-primary-500:hover{--tw-text-opacity:1;color:rgb(54 156 140/var(--tw-text-opacity))}.hover\:text-primary-700:hover{--tw-text-opacity:1;color:rgb(35 98 90/var(--tw-text-opacity))}.hover\:text-primary-800:hover{--tw-text-opacity:1;color:rgb(32 79 74/var(--tw-text-opacity))}.focus\:border-primary-300:focus{--tw-border-opacity:1;border-color:rgb(122 206 189/var(--tw-border-opacity))}.focus\:border-primary-500:focus{--tw-border-opacity:1;border-color:rgb(54 156 140/var(--tw-border-opacity))}.focus\:bg-opacity-50:focus{--tw-bg-opacity:0.5}.focus\:text-primary-800:focus{--tw-text-opacity:1;color:rgb(32 79 74/var(--tw-text-opacity))}.focus\:outline-none:focus{outline:2px solid #0000;outline-offset:2px}.focus\:ring-\[1px\]:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.focus\:ring-\[1px\]:focus,.focus\:ring-\[3px\]:focus{box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus\:ring-\[3px\]:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.focus\:ring-primary-200:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(171 228 214/var(--tw-ring-opacity))}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:opacity-50:disabled{opacity:.5}.group\/btn:hover .group-hover\/btn\:w-2{width:.5rem}.group\/btn:hover .group-hover\/btn\:w-2\.5{width:.625rem}.group:hover .group-hover\:scale-100{--tw-scale-x:1;--tw-scale-y:1}.group:hover .group-hover\:scale-100,.group:hover .group-hover\:scale-x-100{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group:hover .group-hover\:scale-x-100{--tw-scale-x:1}.group:hover .group-hover\:opacity-100,.group\/btn:hover .group-hover\/btn\:opacity-100{opacity:1}@media (min-width:640px){.sm\:left-14{left:3.5rem}.sm\:w-auto{width:auto}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:px-6{padding-left:1.5rem;padding-right:1.5rem}.sm\:pb-40{padding-bottom:10rem}.sm\:pl-20{padding-left:5rem}.sm\:text-3xl{font-size:1.875rem;line-height:2.25rem}.sm\:text-5xl{font-size:3rem;line-height:1}.sm\:text-sm{font-size:.875rem;line-height:1.25rem}}@media (min-width:768px){.md\:block{display:block}.md\:h-screen{height:100vh}.md\:w-16{width:4rem}.md\:w-20{width:5rem}.md\:w-24{width:6rem}.md\:max-w-2xl{max-width:42rem}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.md\:gap-16{gap:4rem}.md\:px-14{padding-left:3.5rem;padding-right:3.5rem}.md\:py-10{padding-top:2.5rem;padding-bottom:2.5rem}.md\:pb-52{padding-bottom:13rem}.md\:text-3xl{font-size:1.875rem;line-height:2.25rem}.md\:text-4xl{font-size:2.25rem;line-height:2.5rem}.md\:text-xl{font-size:1.25rem;line-height:1.75rem}}@media (min-width:1024px){.lg\:left-20{left:5rem}.lg\:order-last{order:9999}.lg\:col-span-3{grid-column:span 3/span 3}.lg\:col-span-4{grid-column:span 4/span 4}.lg\:-my-11{margin-top:-2.75rem;margin-bottom:-2.75rem}.lg\:-mr-9{margin-right:-2.25rem}.lg\:mb-0{margin-bottom:0}.lg\:mb-16{margin-bottom:4rem}.lg\:mt-0{margin-top:0}.lg\:block{display:block}.lg\:flex{display:flex}.lg\:grid{display:grid}.lg\:max-w-4xl{max-width:56rem}.lg\:max-w-7xl{max-width:80rem}.lg\:max-w-max{max-width:-moz-max-content;max-width:max-content}.lg\:max-w-xl{max-width:36rem}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-7{grid-template-columns:repeat(7,minmax(0,1fr))}.lg\:gap-5{gap:1.25rem}.lg\:p-10{padding:2.5rem}.lg\:px-8{padding-left:2rem;padding-right:2rem}.lg\:py-12{padding-top:3rem;padding-bottom:3rem}.lg\:py-24{padding-top:6rem;padding-bottom:6rem}.lg\:pb-20{padding-bottom:5rem}.lg\:pb-60{padding-bottom:15rem}.lg\:pl-28{padding-left:7rem}.lg\:pt-20{padding-top:5rem}.lg\:pt-40{padding-top:10rem}.lg\:text-4xl{font-size:2.25rem;line-height:2.5rem}.lg\:text-5xl{font-size:3rem;line-height:1}.lg\:text-6xl{font-size:3.75rem;line-height:1}.lg\:text-lg{font-size:1.125rem;line-height:1.75rem}}@media (min-width:1280px){.xl\:col-span-3{grid-column:span 3/span 3}.xl\:mx-auto{margin-left:auto;margin-right:auto}.xl\:flex{display:flex}.xl\:hidden{display:none}.xl\:w-auto{width:auto}.xl\:w-full{width:100%}.xl\:max-w-lg{max-width:32rem}.xl\:max-w-xl{max-width:36rem}.xl\:gap-16{gap:4rem}.xl\:gap-x-16{-moz-column-gap:4rem;column-gap:4rem}.xl\:px-20{padding-left:5rem;padding-right:5rem}.xl\:py-20{padding-top:5rem;padding-bottom:5rem}.xl\:py-32{padding-top:8rem}.xl\:pb-32,.xl\:py-32{padding-bottom:8rem}.xl\:pb-\[16\.5rem\]{padding-bottom:16.5rem}.xl\:text-xl{font-size:1.25rem;line-height:1.75rem}} +/*! tailwindcss v3.4.4 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,::backdrop,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.pointer-events-none{pointer-events:none}.pointer-events-auto{pointer-events:auto}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{inset:0}.inset-1{inset:.25rem}.-inset-x-12{left:-3rem;right:-3rem}.inset-x-1{left:.25rem;right:.25rem}.inset-x-4{left:1rem;right:1rem}.-bottom-0{bottom:0}.-bottom-0\.5{bottom:-.125rem}.-bottom-2{bottom:-.5rem}.-bottom-6{bottom:-1.5rem}.-left-1\/4{left:-25%}.-left-2{left:-.5rem}.-top-1\/4{top:-25%}.-top-4{top:-1rem}.-top-6{top:-1.5rem}.bottom-0{bottom:0}.bottom-10{bottom:2.5rem}.bottom-6{bottom:1.5rem}.left-0{left:0}.left-1\/2{left:50%}.left-8{left:2rem}.right-4{right:1rem}.top-0{top:0}.top-1\/4{top:25%}.right-10{right:2.5rem}.right-1{right:.25rem}.right-2{right:.5rem}.right-2\.5{right:.625rem}.top-1\/2{top:50%}.right-1\/2{right:50%}.z-50{z-index:50}.z-\[-1\]{z-index:-1}.z-\[500\]{z-index:500}.col-span-1{grid-column:span 1/span 1}.col-span-10{grid-column:span 10/span 10}.col-span-2{grid-column:span 2/span 2}.col-span-3{grid-column:span 3/span 3}.col-span-4{grid-column:span 4/span 4}.col-span-7{grid-column:span 7/span 7}.-m-1{margin:-.25rem}.-m-1\.5{margin:-.375rem}.m-auto{margin:auto}.mx-auto{margin-left:auto;margin-right:auto}.my-1{margin-top:.25rem;margin-bottom:.25rem}.my-10{margin-top:2.5rem;margin-bottom:2.5rem}.my-2{margin-top:.5rem;margin-bottom:.5rem}.my-3{margin-top:.75rem;margin-bottom:.75rem}.my-4{margin-top:1rem;margin-bottom:1rem}.my-5{margin-top:1.25rem;margin-bottom:1.25rem}.my-6{margin-top:1.5rem;margin-bottom:1.5rem}.-mb-12{margin-bottom:-3rem}.-mb-6{margin-bottom:-1.5rem}.-ml-2{margin-left:-.5rem}.-mr-2{margin-right:-.5rem}.-mr-2\.5{margin-right:-.625rem}.-mt-1{margin-top:-.25rem}.-mt-24{margin-top:-6rem}.-mt-3{margin-top:-.75rem}.mb-0{margin-bottom:0}.mb-14{margin-bottom:3.5rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-7{margin-bottom:1.75rem}.mb-8{margin-bottom:2rem}.mb-9{margin-bottom:2.25rem}.mb-auto{margin-bottom:auto}.mb-px{margin-bottom:1px}.ml-5{margin-left:1.25rem}.mr-2{margin-right:.5rem}.mr-3{margin-right:.75rem}.mt-1{margin-top:.25rem}.mt-1\.5{margin-top:.375rem}.mt-10{margin-top:2.5rem}.mt-12{margin-top:3rem}.mt-16{margin-top:4rem}.mt-2{margin-top:.5rem}.mt-4{margin-top:1rem}.mt-8{margin-top:2rem}.mt-auto{margin-top:auto}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.size-2{width:.5rem;height:.5rem}.size-2\.5{width:.625rem;height:.625rem}.h-12{height:3rem}.h-2{height:.5rem}.h-2\.5{height:.625rem}.h-24{height:6rem}.h-32{height:8rem}.h-8{height:2rem}.h-96{height:24rem}.h-\[150\%\]{height:150%}.h-full{height:100%}.h-px{height:1px}.h-60{height:15rem}.max-h-screen{max-height:100vh}.min-h-screen{min-height:100vh}.w-0{width:0}.w-14{width:3.5rem}.w-16{width:4rem}.w-32{width:8rem}.w-60{width:15rem}.w-\[150\%\]{width:150%}.w-fit{width:-moz-fit-content;width:fit-content}.w-full{width:100%}.w-max{width:-moz-max-content;width:max-content}.w-px{width:1px}.min-w-full{min-width:100%}.max-w-5xl{max-width:64rem}.max-w-6xl{max-width:72rem}.max-w-7xl{max-width:80rem}.max-w-\[94\%\]{max-width:94%}.max-w-full{max-width:100%}.max-w-lg{max-width:32rem}.max-w-md{max-width:28rem}.max-w-none{max-width:none}.max-w-sm{max-width:24rem}.max-w-xl{max-width:36rem}.flex-none{flex:none}.shrink-0{flex-shrink:0}.table-auto{table-layout:auto}.border-collapse{border-collapse:collapse}.origin-bottom{transform-origin:bottom}.-translate-x-1\/2{--tw-translate-x:-50%}.-rotate-3,.-translate-x-1\/2{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-rotate-3{--tw-rotate:-3deg}.rotate-3{--tw-rotate:3deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.scale-0{--tw-scale-x:0;--tw-scale-y:0}.scale-0,.scale-x-0{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.scale-x-0{--tw-scale-x:0}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.transform-gpu{transform:translate3d(var(--tw-translate-x),var(--tw-translate-y),0) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes spin{to{transform:rotate(1turn)}}.animate-spin{animation:spin 1s linear infinite}.cursor-pointer{cursor:pointer}.appearance-none{-webkit-appearance:none;-moz-appearance:none;appearance:none}.grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.flex-col{flex-direction:column}.place-items-center{place-items:center}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.justify-items-center{justify-items:center}.gap-0{gap:0}.gap-0\.5{gap:.125rem}.gap-10{gap:2.5rem}.gap-12{gap:3rem}.gap-20{gap:5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-8{gap:2rem}.gap-x-2{-moz-column-gap:.5rem;column-gap:.5rem}.gap-x-8{-moz-column-gap:2rem;column-gap:2rem}.gap-y-16{row-gap:4rem}.gap-y-4{row-gap:1rem}.space-x-1>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.25rem*var(--tw-space-x-reverse));margin-left:calc(.25rem*(1 - var(--tw-space-x-reverse)))}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.5rem*var(--tw-space-x-reverse));margin-left:calc(.5rem*(1 - var(--tw-space-x-reverse)))}.space-x-20>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(5rem*var(--tw-space-x-reverse));margin-left:calc(5rem*(1 - var(--tw-space-x-reverse)))}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem*var(--tw-space-x-reverse));margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)))}.space-x-5>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1.25rem*var(--tw-space-x-reverse));margin-left:calc(1.25rem*(1 - var(--tw-space-x-reverse)))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem*var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.75rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem*var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem*var(--tw-space-y-reverse))}.space-y-5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.25rem*var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem*var(--tw-space-y-reverse))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse:0;border-top-width:calc(1px*(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px*var(--tw-divide-y-reverse))}.divide-gray-200>:not([hidden])~:not([hidden]){--tw-divide-opacity:1;border-color:rgb(229 231 235/var(--tw-divide-opacity))}.justify-self-start{justify-self:start}.justify-self-end{justify-self:end}.overflow-hidden{overflow:hidden}.overflow-visible{overflow:visible}.overflow-x-auto{overflow-x:auto}.whitespace-normal{white-space:normal}.whitespace-nowrap{white-space:nowrap}.text-wrap{text-wrap:wrap}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-3xl{border-radius:1.5rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-xl{border-radius:.75rem}.rounded-l-\[20px\]{border-top-left-radius:20px;border-bottom-left-radius:20px}.rounded-r-\[20px\]{border-top-right-radius:20px;border-bottom-right-radius:20px}.rounded-r-\[8px\]{border-top-right-radius:8px;border-bottom-right-radius:8px}.rounded-t-full{border-top-left-radius:9999px;border-top-right-radius:9999px}.border{border-width:1px}.border-l-0{border-left-width:0}.border-r-0{border-right-width:0}.border-t-0{border-top-width:0}.border-none{border-style:none}.border-\[\#D7DFE9\]{--tw-border-opacity:1;border-color:rgb(215 223 233/var(--tw-border-opacity))}.border-primary-200{--tw-border-opacity:1;border-color:rgb(171 228 214/var(--tw-border-opacity))}.border-primary-400{--tw-border-opacity:1;border-color:rgb(78 179 161/var(--tw-border-opacity))}.border-primary-500{--tw-border-opacity:1;border-color:rgb(54 156 140/var(--tw-border-opacity))}.border-primary-600{--tw-border-opacity:1;border-color:rgb(40 121 110/var(--tw-border-opacity))}.border-primary-700{--tw-border-opacity:1;border-color:rgb(35 98 90/var(--tw-border-opacity))}.border-y-secondary-300{--tw-border-opacity:1;border-top-color:rgb(255 170 157/var(--tw-border-opacity));border-bottom-color:rgb(255 170 157/var(--tw-border-opacity))}.bg-gray-100{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity))}.bg-gray-500\/25{background-color:#6b728040}.bg-primary-100{--tw-bg-opacity:1;background-color:rgb(213 242 235/var(--tw-bg-opacity))}.bg-primary-200{--tw-bg-opacity:1;background-color:rgb(171 228 214/var(--tw-bg-opacity))}.bg-primary-300{--tw-bg-opacity:1;background-color:rgb(122 206 189/var(--tw-bg-opacity))}.bg-primary-50{--tw-bg-opacity:1;background-color:rgb(243 250 249/var(--tw-bg-opacity))}.bg-primary-500{--tw-bg-opacity:1;background-color:rgb(54 156 140/var(--tw-bg-opacity))}.bg-primary-800{--tw-bg-opacity:1;background-color:rgb(32 79 74/var(--tw-bg-opacity))}.bg-primary-950{--tw-bg-opacity:1;background-color:rgb(13 38 37/var(--tw-bg-opacity))}.bg-secondary-300{--tw-bg-opacity:1;background-color:rgb(255 170 157/var(--tw-bg-opacity))}.bg-secondary-500{--tw-bg-opacity:1;background-color:rgb(255 78 51/var(--tw-bg-opacity))}.bg-secondary-700{--tw-bg-opacity:1;background-color:rgb(200 38 13/var(--tw-bg-opacity))}.bg-transparent{background-color:initial}.bg-gray-400{--tw-bg-opacity:1;background-color:rgb(156 163 175/var(--tw-bg-opacity))}.bg-opacity-0{--tw-bg-opacity:0}.bg-gradient-to-b{background-image:linear-gradient(to bottom,var(--tw-gradient-stops))}.bg-gradient-to-br{background-image:linear-gradient(to bottom right,var(--tw-gradient-stops))}.bg-gradient-to-r{background-image:linear-gradient(to right,var(--tw-gradient-stops))}.bg-gradient-to-t{background-image:linear-gradient(to top,var(--tw-gradient-stops))}.from-gray-100{--tw-gradient-from:#f3f4f6 var(--tw-gradient-from-position);--tw-gradient-to:#f3f4f600 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-primary-900\/50{--tw-gradient-from:#1f423e80 var(--tw-gradient-from-position);--tw-gradient-to:#1f423e00 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-primary-950{--tw-gradient-from:#0d2625 var(--tw-gradient-from-position);--tw-gradient-to:#0d262500 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-transparent{--tw-gradient-from:#0000 var(--tw-gradient-from-position);--tw-gradient-to:#0000 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-10\%{--tw-gradient-from-position:10%}.via-primary-900{--tw-gradient-to:#1f423e00 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),#1f423e var(--tw-gradient-via-position),var(--tw-gradient-to)}.via-primary-900\/50{--tw-gradient-to:#1f423e00 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),#1f423e80 var(--tw-gradient-via-position),var(--tw-gradient-to)}.via-10\%{--tw-gradient-via-position:10%}.to-primary-950{--tw-gradient-to:#0d2625 var(--tw-gradient-to-position)}.to-primary-950\/50{--tw-gradient-to:#0d262580 var(--tw-gradient-to-position)}.to-transparent{--tw-gradient-to:#0000 var(--tw-gradient-to-position)}.to-90\%{--tw-gradient-to-position:90%}.bg-\[length\:100px_auto\]{background-size:100px auto}.bg-cover{background-size:cover}.bg-clip-padding{background-clip:padding-box}.bg-center{background-position:50%}.fill-primary-400{fill:#4eb3a1}.object-contain{-o-object-fit:contain;object-fit:contain}.object-cover{-o-object-fit:cover;object-fit:cover}.object-bottom{-o-object-position:bottom;object-position:bottom}.p-0{padding:0}.p-1{padding:.25rem}.p-1\.5{padding:.375rem}.p-10{padding:2.5rem}.p-16{padding:4rem}.p-2{padding:.5rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.px-10{padding-left:2.5rem;padding-right:2.5rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-40{padding-left:10rem;padding-right:10rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-0{padding-top:0;padding-bottom:0}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-10{padding-top:2.5rem;padding-bottom:2.5rem}.py-16{padding-top:4rem;padding-bottom:4rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-24{padding-top:6rem;padding-bottom:6rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-5{padding-top:1.25rem;padding-bottom:1.25rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.py-8{padding-top:2rem;padding-bottom:2rem}.px-1{padding-left:.25rem;padding-right:.25rem}.pb-12{padding-bottom:3rem}.pb-16{padding-bottom:4rem}.pb-3{padding-bottom:.75rem}.pb-32{padding-bottom:8rem}.pb-8{padding-bottom:2rem}.pl-3{padding-left:.75rem}.pl-4{padding-left:1rem}.pr-3{padding-right:.75rem}.pr-7{padding-right:1.75rem}.pr-\[10px\]{padding-right:10px}.pt-12{padding-top:3rem}.pt-24{padding-top:6rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.text-start{text-align:start}.align-middle{vertical-align:middle}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-5xl{font-size:3rem;line-height:1}.text-7xl{font-size:4.5rem;line-height:1}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-\[450\]{font-weight:450}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.lowercase{text-transform:lowercase}.leading-6{line-height:1.5rem}.leading-none{line-height:1}.tracking-tight{letter-spacing:-.025em}.tracking-wider{letter-spacing:.05em}.text-gray-100{--tw-text-opacity:1;color:rgb(243 244 246/var(--tw-text-opacity))}.text-gray-200{--tw-text-opacity:1;color:rgb(229 231 235/var(--tw-text-opacity))}.text-gray-50{--tw-text-opacity:1;color:rgb(249 250 251/var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.text-gray-600{--tw-text-opacity:1;color:rgb(75 85 99/var(--tw-text-opacity))}.text-gray-700{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity))}.text-gray-800{--tw-text-opacity:1;color:rgb(31 41 55/var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity:1;color:rgb(17 24 39/var(--tw-text-opacity))}.text-primary-300{--tw-text-opacity:1;color:rgb(122 206 189/var(--tw-text-opacity))}.text-primary-400{--tw-text-opacity:1;color:rgb(78 179 161/var(--tw-text-opacity))}.text-primary-50{--tw-text-opacity:1;color:rgb(243 250 249/var(--tw-text-opacity))}.text-primary-500{--tw-text-opacity:1;color:rgb(54 156 140/var(--tw-text-opacity))}.text-primary-600{--tw-text-opacity:1;color:rgb(40 121 110/var(--tw-text-opacity))}.text-primary-800{--tw-text-opacity:1;color:rgb(32 79 74/var(--tw-text-opacity))}.text-primary-900{--tw-text-opacity:1;color:rgb(31 66 62/var(--tw-text-opacity))}.text-primary-950{--tw-text-opacity:1;color:rgb(13 38 37/var(--tw-text-opacity))}.text-secondary-300{--tw-text-opacity:1;color:rgb(255 170 157/var(--tw-text-opacity))}.text-secondary-50{--tw-text-opacity:1;color:rgb(255 243 241/var(--tw-text-opacity))}.text-secondary-500{--tw-text-opacity:1;color:rgb(255 78 51/var(--tw-text-opacity))}.opacity-0{opacity:0}.opacity-50{opacity:.5}.shadow{--tw-shadow:0 1px 3px 0 #0000001a,0 1px 2px -1px #0000001a;--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color)}.shadow,.shadow-lg{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a;--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}.shadow-md{--tw-shadow:0 4px 6px -1px #0000001a,0 2px 4px -2px #0000001a;--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color)}.shadow-md,.shadow-sm{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 2px 0 #0000000d;--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color)}.shadow-gray-800\/5{--tw-shadow-color:#1f29370d;--tw-shadow:var(--tw-shadow-colored)}.outline-0{outline-width:0}.ring-1{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.ring-inset{--tw-ring-inset:inset}.ring-gray-800\/\[\.075\]{--tw-ring-color:rgba(31,41,55,.075)}.ring-primary-100{--tw-ring-opacity:1;--tw-ring-color:rgb(213 242 235/var(--tw-ring-opacity))}.ring-primary-200{--tw-ring-opacity:1;--tw-ring-color:rgb(171 228 214/var(--tw-ring-opacity))}.blur{--tw-blur:blur(8px)}.blur,.blur-3xl{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.blur-3xl{--tw-blur:blur(64px)}.brightness-0{--tw-brightness:brightness(0)}.brightness-0,.drop-shadow{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.drop-shadow{--tw-drop-shadow:drop-shadow(0 1px 2px #0000001a) drop-shadow(0 1px 1px #0000000f)}.drop-shadow-sm{--tw-drop-shadow:drop-shadow(0 1px 1px #0000000d)}.drop-shadow-sm,.invert{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.invert{--tw-invert:invert(100%)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur-sm{--tw-backdrop-blur:blur(4px)}.backdrop-blur-sm,.backdrop-blur-xl{-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.backdrop-blur-xl{--tw-backdrop-blur:blur(24px)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}.duration-500{transition-duration:.5s}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}.\[-webkit-mask-image\:linear-gradient\(to_bottom\2c rgba\(255\2c 255\2c 255\2c 1\)_75\%\2c rgba\(255\2c 255\2c 255\2c 0\)\)\]{-webkit-mask-image:linear-gradient(180deg,#fff 75%,#fff0)}.\[-webkit-mask-image\:linear-gradient\(to_top\2c rgba\(255\2c 255\2c 255\2c 1\)_75\%\2c rgba\(255\2c 255\2c 255\2c 0\)\)\]{-webkit-mask-image:linear-gradient(0deg,#fff 75%,#fff0)}.hover\:border-primary-200:hover{--tw-border-opacity:1;border-color:rgb(171 228 214/var(--tw-border-opacity))}.hover\:bg-gray-200:hover{--tw-bg-opacity:1;background-color:rgb(229 231 235/var(--tw-bg-opacity))}.hover\:bg-primary-700:hover{--tw-bg-opacity:1;background-color:rgb(35 98 90/var(--tw-bg-opacity))}.hover\:bg-primary-800:hover{--tw-bg-opacity:1;background-color:rgb(32 79 74/var(--tw-bg-opacity))}.hover\:bg-secondary-700:hover{--tw-bg-opacity:1;background-color:rgb(200 38 13/var(--tw-bg-opacity))}.hover\:bg-gray-700:hover{--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity))}.hover\:bg-opacity-50:hover{--tw-bg-opacity:0.5}.hover\:font-bold:hover{font-weight:700}.hover\:text-primary-400:hover{--tw-text-opacity:1;color:rgb(78 179 161/var(--tw-text-opacity))}.hover\:text-primary-50:hover{--tw-text-opacity:1;color:rgb(243 250 249/var(--tw-text-opacity))}.hover\:text-primary-500:hover{--tw-text-opacity:1;color:rgb(54 156 140/var(--tw-text-opacity))}.hover\:text-primary-700:hover{--tw-text-opacity:1;color:rgb(35 98 90/var(--tw-text-opacity))}.hover\:text-primary-800:hover{--tw-text-opacity:1;color:rgb(32 79 74/var(--tw-text-opacity))}.focus\:border-primary-300:focus{--tw-border-opacity:1;border-color:rgb(122 206 189/var(--tw-border-opacity))}.focus\:border-primary-500:focus{--tw-border-opacity:1;border-color:rgb(54 156 140/var(--tw-border-opacity))}.focus\:bg-opacity-50:focus{--tw-bg-opacity:0.5}.focus\:text-primary-800:focus{--tw-text-opacity:1;color:rgb(32 79 74/var(--tw-text-opacity))}.focus\:outline-none:focus{outline:2px solid #0000;outline-offset:2px}.focus\:ring-\[1px\]:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.focus\:ring-\[1px\]:focus,.focus\:ring-\[3px\]:focus{box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus\:ring-\[3px\]:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.focus\:ring-primary-200:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(171 228 214/var(--tw-ring-opacity))}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:opacity-50:disabled{opacity:.5}.group\/btn:hover .group-hover\/btn\:w-2{width:.5rem}.group\/btn:hover .group-hover\/btn\:w-2\.5{width:.625rem}.group:hover .group-hover\:scale-100{--tw-scale-x:1;--tw-scale-y:1}.group:hover .group-hover\:scale-100,.group:hover .group-hover\:scale-x-100{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group:hover .group-hover\:scale-x-100{--tw-scale-x:1}.group:hover .group-hover\:opacity-100,.group\/btn:hover .group-hover\/btn\:opacity-100{opacity:1}@media (min-width:640px){.sm\:left-14{left:3.5rem}.sm\:w-auto{width:auto}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:px-6{padding-left:1.5rem;padding-right:1.5rem}.sm\:pb-40{padding-bottom:10rem}.sm\:pl-20{padding-left:5rem}.sm\:text-3xl{font-size:1.875rem;line-height:2.25rem}.sm\:text-5xl{font-size:3rem;line-height:1}.sm\:text-sm{font-size:.875rem;line-height:1.25rem}}@media (min-width:768px){.md\:block{display:block}.md\:h-screen{height:100vh}.md\:w-16{width:4rem}.md\:w-20{width:5rem}.md\:w-24{width:6rem}.md\:max-w-2xl{max-width:42rem}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.md\:gap-16{gap:4rem}.md\:px-14{padding-left:3.5rem;padding-right:3.5rem}.md\:py-10{padding-top:2.5rem;padding-bottom:2.5rem}.md\:pb-52{padding-bottom:13rem}.md\:text-3xl{font-size:1.875rem;line-height:2.25rem}.md\:text-4xl{font-size:2.25rem;line-height:2.5rem}.md\:text-xl{font-size:1.25rem;line-height:1.75rem}}@media (min-width:1024px){.lg\:left-20{left:5rem}.lg\:order-last{order:9999}.lg\:col-span-3{grid-column:span 3/span 3}.lg\:col-span-4{grid-column:span 4/span 4}.lg\:-my-11{margin-top:-2.75rem;margin-bottom:-2.75rem}.lg\:-mr-9{margin-right:-2.25rem}.lg\:mb-0{margin-bottom:0}.lg\:mb-16{margin-bottom:4rem}.lg\:mt-0{margin-top:0}.lg\:block{display:block}.lg\:flex{display:flex}.lg\:grid{display:grid}.lg\:max-w-4xl{max-width:56rem}.lg\:max-w-7xl{max-width:80rem}.lg\:max-w-max{max-width:-moz-max-content;max-width:max-content}.lg\:max-w-xl{max-width:36rem}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-7{grid-template-columns:repeat(7,minmax(0,1fr))}.lg\:gap-5{gap:1.25rem}.lg\:p-10{padding:2.5rem}.lg\:px-8{padding-left:2rem;padding-right:2rem}.lg\:py-12{padding-top:3rem;padding-bottom:3rem}.lg\:py-24{padding-top:6rem;padding-bottom:6rem}.lg\:pb-20{padding-bottom:5rem}.lg\:pb-60{padding-bottom:15rem}.lg\:pl-28{padding-left:7rem}.lg\:pt-20{padding-top:5rem}.lg\:pt-40{padding-top:10rem}.lg\:text-4xl{font-size:2.25rem;line-height:2.5rem}.lg\:text-5xl{font-size:3rem;line-height:1}.lg\:text-6xl{font-size:3.75rem;line-height:1}.lg\:text-lg{font-size:1.125rem;line-height:1.75rem}}@media (min-width:1280px){.xl\:col-span-3{grid-column:span 3/span 3}.xl\:mx-auto{margin-left:auto;margin-right:auto}.xl\:flex{display:flex}.xl\:hidden{display:none}.xl\:w-auto{width:auto}.xl\:w-full{width:100%}.xl\:max-w-lg{max-width:32rem}.xl\:max-w-xl{max-width:36rem}.xl\:gap-16{gap:4rem}.xl\:gap-x-16{-moz-column-gap:4rem;column-gap:4rem}.xl\:px-20{padding-left:5rem;padding-right:5rem}.xl\:py-20{padding-top:5rem;padding-bottom:5rem}.xl\:py-32{padding-top:8rem}.xl\:pb-32,.xl\:py-32{padding-bottom:8rem}.xl\:pb-\[16\.5rem\]{padding-bottom:16.5rem}.xl\:text-xl{font-size:1.25rem;line-height:1.75rem}} diff --git a/unikernel.ml b/unikernel.ml index 98c61410..fcc6aeb1 100644 --- a/unikernel.ml +++ b/unikernel.ml @@ -1079,6 +1079,50 @@ struct (Utils.Status.to_json status) `Internal_server_error)) + let view_user albatross store uuid reqd (user : User_model.user) = + let users = User_model.create_user_uuid_map (snd store).Storage.users in + match User_model.find_user_by_key uuid users with + | Some u -> + (Albatross.query albatross ~domain:u.name + (`Unikernel_cmd `Unikernel_info) + >|= function + | Error msg -> + Logs.err (fun m -> + m "error while communicating with albatross: %s" msg); + [] + | Ok (_hdr, `Success (`Unikernel_info unikernels)) -> unikernels + | Ok reply -> + Logs.err (fun m -> + m "expected a unikernel info reply, received %a" + (Vmm_commands.pp_wire ~verbose:false) + reply); + []) + >>= fun unikernels -> + Lwt.return + (reply reqd ~content_type:"text/html" + (Dashboard.dashboard_layout user + ~page_title:(u.name ^ " | Mollymawk") + ~content: + (User_single.user_single_layout u unikernels + (Ptime.v (P.now_d_ps ()))) + ~icon:"/images/robur.png" ()) + `OK) + | None -> + let status = + { + Utils.Status.code = 404; + title = "Error"; + data = "Couldn't find account with uuid: " ^ uuid; + success = false; + } + in + Lwt.return + (reply reqd ~content_type:"text/html" + (Guest_layout.guest_layout ~page_title:"404 | Mollymawk" + ~content:(Error_page.error_layout status) + ~icon:"/images/robur.png" ()) + `Not_found) + let request_handler stack albatross js_file css_file imgs store (_ipaddr, _port) reqd = Lwt.async (fun () -> @@ -1159,6 +1203,12 @@ struct | "/admin/users" -> check_meth `GET (fun () -> authenticate ~check_admin:true !store reqd (users !store reqd)) + | path when String.(length path >= 12 && sub path 0 12 = "/admin/user/") + -> + check_meth `GET (fun () -> + let uuid = String.sub path 12 (String.length path - 12) in + authenticate ~check_admin:true !store reqd + (view_user !albatross !store uuid reqd)) | "/admin/settings" -> check_meth `GET (fun () -> authenticate ~check_admin:true !store reqd diff --git a/user_single.ml b/user_single.ml new file mode 100644 index 00000000..592b94f7 --- /dev/null +++ b/user_single.ml @@ -0,0 +1,100 @@ +let user_single_layout (user : User_model.user) unikernels current_time = + Tyxml_html.( + section + ~a:[ a_class [ "col-span-7 p-4 bg-gray-50 my-1" ] ] + [ + section + ~a:[ a_class [ "flex-col justify-center" ] ] + [ + div + ~a:[ a_class [ "text-center" ] ] + [ + i ~a:[ a_class [ "fa-solid fa-circle-user text-7xl" ] ] []; + p + ~a:[ a_class [ "text-3xl font-semibold uppercase" ] ] + [ txt user.name ]; + div + ~a: + [ + a_class + [ "flex justify-center justify-items-center space-x-1" ]; + ] + [ + p ~a:[ a_class [ "text-lg" ] ] [ txt user.email ]; + (match user.email_verified with + | Some ptime -> + span + [ + i ~a:[ a_class [ "fa-solid fa-check" ] ] []; + i + ~a:[ a_class [ "text-xs" ] ] + [ + txt + (Utils.TimeHelper.time_ago current_time ptime); + ]; + ] + | None -> + i + ~a:[ a_class [ "fa-solid fa-x text-secondary-500" ] ] + []); + ]; + ]; + div + ~a:[ a_class [ "flex justify-center space-x-4 my-4" ] ] + [ + (if user.active then + button + ~a: + [ + a_onclick ("toggleUserStatus('" ^ user.uuid ^ "')"); + a_class + [ + "px-3 py-2 rounded bg-secondary-500 \ + text-secondary-50 hover:bg-secondary-700 \ + font-semibold"; + ]; + ] + [ txt "Deactivate" ] + else + button + ~a: + [ + a_onclick ("toggleUserStatus('" ^ user.uuid ^ "')"); + a_class + [ + "px-3 py-2 rounded bg-primary-500 text-primary-50 \ + hover:bg-primary-700 font-semibold"; + ]; + ] + [ txt "Activate" ]); + (if user.super_user then + button + ~a: + [ + a_onclick ("toggleUserStatus('" ^ user.uuid ^ "')"); + a_class + [ + "px-3 py-2 rounded bg-secondary-500 \ + text-secondary-50 hover:bg-secondary-700 \ + font-semibold"; + ]; + ] + [ txt "Remove Admin" ] + else + button + ~a: + [ + a_onclick ("toggleUserStatus('" ^ user.uuid ^ "')"); + a_class + [ + "px-3 py-2 rounded bg-primary-500 text-primary-50 \ + hover:bg-primary-700 font-semibold"; + ]; + ] + [ txt "Make Admin" ]); + ]; + ]; + section + ~a:[ a_class [ "my-4" ] ] + [ Unikernel_index.unikernel_index_layout unikernels current_time ]; + ]) diff --git a/users_index.ml b/users_index.ml index 2796807d..e4f77cd5 100644 --- a/users_index.ml +++ b/users_index.ml @@ -266,40 +266,21 @@ let users_index_layout (users : User_model.user list) current_time = ]; ] [ - (if user.active then - button - ~a: - [ - a_onclick - ("toggleUserStatus('" - ^ user.uuid ^ "')"); - a_class - [ - "px-3 py-2 rounded \ - bg-secondary-500 \ - text-secondary-50 \ - hover:bg-secondary-700 \ - font-semibold"; - ]; - ] - [ txt "Deactivate" ] - else - button - ~a: - [ - a_onclick - ("toggleUserStatus('" - ^ user.uuid ^ "')"); - a_class - [ - "px-3 py-2 rounded \ - bg-primary-500 \ - text-primary-50 \ - hover:bg-primary-700 \ - font-semibold"; - ]; - ] - [ txt "Activate" ]); + a + ~a: + [ + a_href + ("/admin/user/" ^ user.uuid ^ ""); + a_class + [ + "border border-primary-500 \ + hover:bg-primary-700 px-2 \ + py-1 text-primary-800 \ + hover:text-primary-50 \ + rounded"; + ]; + ] + [ txt "View" ]; ]; ]) users); From 0f37ae697070d1c4abcef6d9f5537ede7b2d2b16 Mon Sep 17 00:00:00 2001 From: PizieDust Date: Mon, 9 Sep 2024 09:22:07 +0200 Subject: [PATCH 15/21] refactor toggling functions --- assets/main.js | 40 ++++++----- unikernel.ml | 175 ++++++++++++++++++++----------------------------- user_single.ml | 5 +- 3 files changed, 99 insertions(+), 121 deletions(-) diff --git a/assets/main.js b/assets/main.js index c9b682d6..2920cf3a 100644 --- a/assets/main.js +++ b/assets/main.js @@ -241,20 +241,30 @@ function buttonLoading(btn, load, text) { } } -async function toggleUserStatus(uuid) { - try { - const response = await fetch("/api/admin/user/status/toggle", { - method: 'POST', - body: JSON.stringify({"uuid":uuid}) - }) - const data = await response.json(); - if(data.status === 200) { +async function toggleUserStatus(uuid, endpoint) { + try { + const response = await fetch(endpoint, { + method: 'POST', + body: JSON.stringify({ uuid: uuid }), + headers: { 'Content-Type': 'application/json' } + }); + + const data = await response.json(); + if (response.status === 200) { postAlert("bg-primary-300", data.data); - setTimeout(function () { window.location.reload()}, 1000); - } else { + setTimeout(() => window.location.reload(), 1000); + } else { postAlert("bg-secondary-300", data.data); - } - } catch (error) { - postAlert("bg-secondary-300", error); - } -} + } + } catch (error) { + postAlert("bg-secondary-300", error); + } + } + + async function toggleUserActiveStatus(uuid) { + await toggleUserStatus(uuid, "/api/admin/user/activate/toggle"); + } + + async function toggleUserAdminStatus(uuid) { + await toggleUserStatus(uuid, "/api/admin/user/admin/toggle"); + } diff --git a/unikernel.ml b/unikernel.ml index fcc6aeb1..0f2354cb 100644 --- a/unikernel.ml +++ b/unikernel.ml @@ -136,6 +136,13 @@ struct let resp = Httpaf.Response.create ~headers status in Httpaf.Reqd.respond_with_string reqd resp data + let http_response reqd ~status_code ~title ~data ~success http_status = + let status = { Utils.Status.code = status_code; title; data; success } in + Lwt.return + (reply reqd ~content_type:"application/json" + (Utils.Status.to_json status) + http_status) + let sign_up reqd = match Middleware.has_session_cookie reqd with | Some cookie -> ( @@ -505,7 +512,8 @@ struct `Bad_request) | Error (`Msg s) -> Middleware.redirect_to_login reqd ~msg:s () - let toggle_user store reqd _user = + let toggle_account_attribute store reqd ~key update_fn error_on_last + ~error_message = decode_request_body reqd >>= fun data -> let json = try Ok (Yojson.Basic.from_string data) @@ -513,19 +521,9 @@ struct in match json with | Error (`Msg err) -> - Logs.warn (fun m -> m "Failed to parse JSON: %s" err); - let status = - { - Utils.Status.code = 400; - title = "Error"; - data = String.escaped err; - success = false; - } - in - Lwt.return - (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) - `Bad_request) + Logs.warn (fun m -> m "Failed to parse JSON: %s - %s" key err); + http_response reqd ~status_code:400 ~title:"Error" ~data:err + ~success:false `Bad_request | Ok (`Assoc json) -> ( match Utils.Json.get "uuid" json with | Some (`String uuid) -> ( @@ -534,101 +532,66 @@ struct in match List.assoc_opt uuid users with | None -> - let status = - { - Utils.Status.code = 404; - title = "Error"; - data = "Account not found"; - success = false; - } - in - Lwt.return - (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) - `Not_found) + Logs.warn (fun m -> m "%s : Account not found" key); + http_response reqd ~status_code:404 ~title:"Error" + ~data:"Account not found" ~success:false `Not_found | Some user -> ( - let is_last_active_user = - user.active - && List.length - (List.filter - (fun u -> u.User_model.active) - (snd !store).Storage.users) - <= 1 - in - if is_last_active_user then - let status = - { - Utils.Status.code = 403; - title = "Error"; - data = "Refusing to deactivate last active user"; - success = false; - } - in - Lwt.return - (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) - `Forbidden) + if error_on_last user then ( + Logs.warn (fun m -> + m "%s : Can't perform action on last user" key); + http_response reqd ~status_code:403 ~title:"Error" + ~data:error_message ~success:false `Forbidden) else - let user = - User_model.update_user user ~active:(not user.active) - ~updated_at:(Ptime.v (P.now_d_ps ())) - () - in - Store.update_user !store user >>= function + let updated_user = update_fn user in + Store.update_user !store updated_user >>= function | Ok store' -> store := store'; - let status = - { - Utils.Status.code = 200; - title = "OK"; - data = "Updated user successfully"; - success = true; - } - in - Lwt.return - (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) - `OK) + http_response reqd ~status_code:200 ~title:"OK" + ~data:"Updated user successfully" ~success:true `OK | Error (`Msg msg) -> - let status = - { - Utils.Status.code = 500; - title = "Error"; - data = String.escaped msg; - success = false; - } - in - Lwt.return - (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) - `Internal_server_error))) + Logs.warn (fun m -> + m "%s : Storage error with %s" key msg); + http_response reqd ~status_code:500 ~title:"Error" + ~data:msg ~success:false `Internal_server_error)) | _ -> - Logs.warn (fun m -> m "Failed to parse JSON - no UUID found"); - let status = - { - Utils.Status.code = 404; - title = "Error"; - data = "Couldn't find a UUID in the json."; - success = false; - } - in - Lwt.return - (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) - `Not_found)) + Logs.warn (fun m -> + m "%s: Failed to parse JSON - no UUID found" key); + http_response reqd ~status_code:404 ~title:"Error" + ~data:"Couldn't find a UUID in the JSON." ~success:false + `Not_found) | Ok _ -> - let status = - { - Utils.Status.code = 400; - title = "Error"; - data = "Provided JSON is not a dictionary"; - success = false; - } - in - Lwt.return - (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) - `Bad_request) + http_response reqd ~status_code:400 ~title:"Error" + ~data:"Provided JSON is not a dictionary" ~success:false `Bad_request + + let toggle_account_activation store reqd _user = + toggle_account_attribute store reqd ~key:"active" + (fun user -> + User_model.update_user user ~active:(not user.active) + ~updated_at:(Ptime.v (P.now_d_ps ())) + ()) + (fun user -> + user.active + && List.length + (List.filter + (fun u -> u.User_model.active) + (snd !store).Storage.users) + <= 1) + ~error_message:"Refusing to deactivate last active user" + + let toggle_admin_activation store reqd _user = + toggle_account_attribute store reqd ~key:"super_user" + (fun user -> + User_model.update_user user ~super_user:(not user.super_user) + ~updated_at:(Ptime.v (P.now_d_ps ())) + ()) + (fun user -> + user.super_user + && List.length + (List.filter + (fun u -> u.User_model.super_user) + (snd !store).Storage.users) + <= 1) + ~error_message:"Refusing to remove last administrator" let dashboard albatross reqd (user : User_model.user) = (* TODO use uuid in the future *) @@ -1217,10 +1180,14 @@ struct check_meth `POST (fun () -> authenticate ~check_admin:true ~api_meth:true !store reqd (update_settings stack store albatross reqd)) - | "/api/admin/user/status/toggle" -> + | "/api/admin/user/activate/toggle" -> + check_meth `POST (fun () -> + authenticate ~check_admin:true ~api_meth:true !store reqd + (toggle_account_activation store reqd)) + | "/api/admin/user/admin/toggle" -> check_meth `POST (fun () -> authenticate ~check_admin:true ~api_meth:true !store reqd - (toggle_user store reqd)) + (toggle_admin_activation store reqd)) | "/api/unikernels" -> check_meth `GET (fun () -> authenticate ~api_meth:true !store reqd diff --git a/user_single.ml b/user_single.ml index 592b94f7..545f0880 100644 --- a/user_single.ml +++ b/user_single.ml @@ -71,7 +71,8 @@ let user_single_layout (user : User_model.user) unikernels current_time = button ~a: [ - a_onclick ("toggleUserStatus('" ^ user.uuid ^ "')"); + a_onclick + ("toggleUserActiveStatus('" ^ user.uuid ^ "')"); a_class [ "px-3 py-2 rounded bg-secondary-500 \ @@ -84,7 +85,7 @@ let user_single_layout (user : User_model.user) unikernels current_time = button ~a: [ - a_onclick ("toggleUserStatus('" ^ user.uuid ^ "')"); + a_onclick ("toggleUserAdminStatus('" ^ user.uuid ^ "')"); a_class [ "px-3 py-2 rounded bg-primary-500 text-primary-50 \ From 70ae199ec93409368f9e341ffe1755a6dd215497 Mon Sep 17 00:00:00 2001 From: PizieDust Date: Mon, 9 Sep 2024 10:33:03 +0200 Subject: [PATCH 16/21] fix function bug --- user_single.ml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/user_single.ml b/user_single.ml index 545f0880..9a1de347 100644 --- a/user_single.ml +++ b/user_single.ml @@ -46,7 +46,8 @@ let user_single_layout (user : User_model.user) unikernels current_time = button ~a: [ - a_onclick ("toggleUserStatus('" ^ user.uuid ^ "')"); + a_onclick + ("toggleUserActiveStatus('" ^ user.uuid ^ "')"); a_class [ "px-3 py-2 rounded bg-secondary-500 \ @@ -59,7 +60,8 @@ let user_single_layout (user : User_model.user) unikernels current_time = button ~a: [ - a_onclick ("toggleUserStatus('" ^ user.uuid ^ "')"); + a_onclick + ("toggleUserActiveStatus('" ^ user.uuid ^ "')"); a_class [ "px-3 py-2 rounded bg-primary-500 text-primary-50 \ @@ -71,8 +73,7 @@ let user_single_layout (user : User_model.user) unikernels current_time = button ~a: [ - a_onclick - ("toggleUserActiveStatus('" ^ user.uuid ^ "')"); + a_onclick ("toggleUserAdminStatus('" ^ user.uuid ^ "')"); a_class [ "px-3 py-2 rounded bg-secondary-500 \ From acbf0b87ed86a6129fab003afb2312a5c2eaab48 Mon Sep 17 00:00:00 2001 From: PizieDust Date: Mon, 9 Sep 2024 10:33:34 +0200 Subject: [PATCH 17/21] refactor http responses --- unikernel.ml | 545 ++++++++++++--------------------------------------- 1 file changed, 121 insertions(+), 424 deletions(-) diff --git a/unikernel.ml b/unikernel.ml index 0f2354cb..ab94789c 100644 --- a/unikernel.ml +++ b/unikernel.ml @@ -136,10 +136,11 @@ struct let resp = Httpaf.Response.create ~headers status in Httpaf.Reqd.respond_with_string reqd resp data - let http_response reqd ~status_code ~title ~data ~success http_status = + let http_response reqd ?(header_list = []) ~status_code ~title ~data ~success + http_status = let status = { Utils.Status.code = status_code; title; data; success } in Lwt.return - (reply reqd ~content_type:"application/json" + (reply reqd ~content_type:"application/json" ~header_list (Utils.Status.to_json status) http_status) @@ -184,18 +185,8 @@ struct match json with | Error (`Msg err) -> Logs.warn (fun m -> m "Failed to parse JSON: %s" err); - let status = - { - Utils.Status.code = 400; - title = "Error"; - data = String.escaped err; - success = false; - } - in - Lwt.return - (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) - `Bad_request) + http_response reqd ~status_code:400 ~title:"Error" + ~data:(String.escaped err) ~success:false `Bad_request | Ok json -> ( let validate_user_input ~name ~email ~password = if name = "" || email = "" || password = "" then @@ -219,36 +210,17 @@ struct in match validate_user_input ~name ~email ~password with | Error err -> - let status = - { - Utils.Status.code = 400; - title = "Error"; - data = String.escaped err; - success = false; - } - in - Lwt.return - (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) - `Bad_request) + http_response reqd ~status_code:400 ~title:"Error" + ~data:(String.escaped err) ~success:false `Bad_request | Ok _ -> ( let _, (s : Storage.t) = !store in let users = s.users in let user = User_model.check_if_user_exists email users in match user with | Some _ -> - let status = - { - Utils.Status.code = 400; - title = "Error"; - data = "A user with this email already exist."; - success = false; - } - in - Lwt.return - (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) - `Bad_request) + http_response reqd ~status_code:400 ~title:"Error" + ~data:"A user with this email already exist." ~success:false + `Bad_request | None -> ( let created_at = Ptime.v (P.now_d_ps ()) in let user = @@ -262,15 +234,6 @@ struct Store.add_user !store user >>= function | Ok store' -> store := store'; - let status = - { - Utils.Status.code = 200; - title = "Success"; - data = - Yojson.Basic.to_string (User_model.user_to_json user); - success = true; - } - in let cookie = List.find (fun (c : User_model.cookie) -> @@ -285,23 +248,14 @@ struct ("Set-Cookie", cookie_value); ("location", "/dashboard"); ] in - Lwt.return - (reply reqd ~header_list ~content_type:"application/json" - (Utils.Status.to_json status) - `OK) + http_response reqd ~header_list ~status_code:200 + ~title:"Success" + ~data: + (Yojson.Basic.to_string (User_model.user_to_json user)) + ~success:true `OK | Error (`Msg err) -> - let status = - { - Utils.Status.code = 400; - title = "Error"; - data = String.escaped err; - success = false; - } - in - Lwt.return - (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) - `Bad_request)))) + http_response reqd ~status_code:400 ~title:"Error" + ~data:(String.escaped err) ~success:false `Bad_request))) let login store reqd = decode_request_body reqd >>= fun data -> @@ -312,18 +266,8 @@ struct match json with | Error (`Msg err) -> Logs.warn (fun m -> m "Failed to parse JSON: %s" err); - let status = - { - Utils.Status.code = 400; - title = "Error"; - data = String.escaped err; - success = false; - } - in - Lwt.return - (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) - `Bad_request) + http_response reqd ~status_code:400 ~title:"Error" + ~data:(String.escaped err) ~success:false `Bad_request | Ok json -> ( let validate_user_input ~email ~password = if email = "" || password = "" then Error "All fields must be filled." @@ -341,18 +285,8 @@ struct in match validate_user_input ~email ~password with | Error err -> - let status = - { - Utils.Status.code = 400; - title = "Error"; - data = String.escaped err; - success = false; - } - in - Lwt.return - (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) - `Bad_request) + http_response reqd ~status_code:400 ~title:"Error" + ~data:(String.escaped err) ~success:false `Bad_request | Ok _ -> ( let now = Ptime.v (P.now_d_ps ()) in let _, (t : Storage.t) = !store in @@ -360,31 +294,13 @@ struct let login = User_model.login_user ~email ~password users now in match login with | Error (`Msg err) -> - let status = - { - Utils.Status.code = 400; - title = "Error"; - data = String.escaped err; - success = false; - } - in - Lwt.return - (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) - `Bad_request) + http_response reqd ~status_code:400 ~title:"Error" + ~data:(String.escaped err) ~success:false `Bad_request | Ok user -> ( Store.update_user !store user >>= function | Ok store' -> ( store := store'; - let status = - { - Utils.Status.code = 200; - title = "Success"; - data = - Yojson.Basic.to_string (User_model.user_to_json user); - success = true; - } - in + let cookie = List.find_opt (fun (c : User_model.cookie) -> @@ -403,39 +319,23 @@ struct ("location", "/dashboard"); ] in - Lwt.return - (reply reqd ~header_list - ~content_type:"application/json" - (Utils.Status.to_json status) - `OK) + + http_response reqd ~header_list ~status_code:200 + ~title:"Success" + ~data: + (Yojson.Basic.to_string + (User_model.user_to_json user)) + ~success:true `OK | None -> - let status = - { - Utils.Status.code = 400; - title = "Error"; - data = - "Something went wrong. Wait a few seconds and \ - try again."; - success = false; - } - in - Lwt.return - (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) - `Bad_request)) - | Error (`Msg msg) -> - let status = - { - Utils.Status.code = 500; - title = "Error"; - data = String.escaped msg; - success = false; - } - in - Lwt.return - (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) - `Bad_request)))) + http_response reqd ~status_code:500 ~title:"Error" + ~data: + "Something went wrong. Wait a few seconds and try \ + again." + ~success:false `Internal_server_error) + | Error (`Msg err) -> + http_response reqd ~status_code:500 ~title:"Error" + ~data:(String.escaped err) ~success:false + `Internal_server_error))) let verify_email store reqd user = let email_verification_uuid = User_model.generate_uuid () in @@ -455,19 +355,9 @@ struct (reply reqd ~content_type:"text/html" (Verify_email.verify_page ~user ~icon:"/images/robur.png" ()) `OK) - | Error (`Msg msg) -> - let status = - { - Utils.Status.code = 500; - title = "Error"; - data = String.escaped msg; - success = false; - } - in - Lwt.return - (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) - `Internal_server_error) + | Error (`Msg err) -> + http_response reqd ~status_code:500 ~title:"Error" + ~data:(String.escaped err) ~success:false `Internal_server_error let verify_email_token store reqd verification_token (user : User_model.user) = @@ -485,31 +375,12 @@ struct store := store'; Middleware.redirect_to_dashboard reqd () | Error (`Msg msg) -> - let status = - { - Utils.Status.code = 500; - title = "Error"; - data = String.escaped msg; - success = false; - } - in - Lwt.return - (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) - `Internal_server_error) + http_response reqd ~status_code:500 ~title:"Error" + ~data:(String.escaped msg) ~success:false `Internal_server_error else - let status = - { - Utils.Status.code = 400; - title = "Error"; - data = "Logged in user is not the to-be-verified one"; - success = false; - } - in - Lwt.return - (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) - `Bad_request) + http_response reqd ~status_code:400 ~title:"Error" + ~data:"Logged in user is not the to-be-verified one" ~success:false + `Bad_request | Error (`Msg s) -> Middleware.redirect_to_login reqd ~msg:s () let toggle_account_attribute store reqd ~key update_fn error_on_last @@ -564,7 +435,7 @@ struct ~data:"Provided JSON is not a dictionary" ~success:false `Bad_request let toggle_account_activation store reqd _user = - toggle_account_attribute store reqd ~key:"active" + toggle_account_attribute store reqd ~key:"toggle-active-account" (fun user -> User_model.update_user user ~active:(not user.active) ~updated_at:(Ptime.v (P.now_d_ps ())) @@ -576,10 +447,10 @@ struct (fun u -> u.User_model.active) (snd !store).Storage.users) <= 1) - ~error_message:"Refusing to deactivate last active user" + ~error_message:"Cannot deactivate last active user" let toggle_admin_activation store reqd _user = - toggle_account_attribute store reqd ~key:"super_user" + toggle_account_attribute store reqd ~key:"toggle-admin-account" (fun user -> User_model.update_user user ~super_user:(not user.super_user) ~updated_at:(Ptime.v (P.now_d_ps ())) @@ -591,7 +462,7 @@ struct (fun u -> u.User_model.super_user) (snd !store).Storage.users) <= 1) - ~error_message:"Refusing to remove last administrator" + ~error_message:"Cannot remove last administrator" let dashboard albatross reqd (user : User_model.user) = (* TODO use uuid in the future *) @@ -647,18 +518,8 @@ struct match json with | Error (`Msg err) -> Logs.warn (fun m -> m "Failed to parse JSON: %s" err); - let status = - { - Utils.Status.code = 400; - title = "Error"; - data = String.escaped err; - success = false; - } - in - Lwt.return - (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) - `Bad_request) + http_response reqd ~status_code:400 ~title:"Error" + ~data:(String.escaped err) ~success:false `Bad_request | Ok json -> ( match Configuration.of_json_from_http json (Ptime.v (P.now_d_ps ())) @@ -674,44 +535,15 @@ struct configuration_settings.private_key >>= fun new_albatross -> albatross := new_albatross; - let status = - { - Utils.Status.code = 200; - title = "Success"; - data = "Configuration updated successfully"; - success = true; - } - in - Lwt.return - (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) - `OK) + http_response reqd ~status_code:200 ~title:"Success" + ~data:"Configuration updated successfully" ~success:true `OK | Error (`Msg err) -> - let status = - { - Utils.Status.code = 500; - title = "Error"; - data = String.escaped err; - success = false; - } - in - Lwt.return - (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) - `Internal_server_error)) + http_response reqd ~status_code:500 ~title:"Error" + ~data:(String.escaped err) ~success:false + `Internal_server_error) | Error (`Msg err) -> - let status = - { - Utils.Status.code = 500; - title = "Error"; - data = String.escaped err; - success = false; - } - in - Lwt.return - (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) - `Internal_server_error)) + http_response reqd ~status_code:500 ~title:"Error" + ~data:(String.escaped err) ~success:false `Bad_request) let deploy_form reqd user = Lwt.return @@ -725,47 +557,23 @@ struct let unikernel_info albatross reqd (user : User_model.user) = (* TODO use uuid in the future *) Albatross.query albatross ~domain:user.name (`Unikernel_cmd `Unikernel_info) - >|= function + >>= function | Error msg -> - let status = - { - Utils.Status.code = 500; - title = "Error"; - data = - Yojson.Safe.to_string - (`String ("Error while querying albatross: " ^ msg)); - success = false; - } - in - reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) - `Internal_server_error + http_response reqd ~status_code:500 ~title:"Error" + ~data: + (Yojson.Safe.to_string + (`String ("Error while querying albatross: " ^ msg))) + ~success:false `Internal_server_error | Ok (_hdr, res) -> ( match Albatross_json.res res with | Ok res -> - let status = - { - Utils.Status.code = 200; - title = "Success"; - data = Yojson.Safe.to_string res; - success = true; - } - in - reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) - `OK + http_response reqd ~status_code:200 ~title:"Success" + ~data:(Yojson.Safe.to_string res) + ~success:true `OK | Error (`String res) -> - let status = - { - Utils.Status.code = 500; - title = "Error"; - data = Yojson.Safe.to_string (`String res); - success = false; - } - in - reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) - `Internal_server_error) + http_response reqd ~status_code:500 ~title:"Error" + ~data:(Yojson.Safe.to_string (`String res)) + ~success:false `Internal_server_error) let unikernel_info_one albatross name reqd (user : User_model.user) = (* TODO use uuid in the future *) @@ -814,46 +622,22 @@ struct (* TODO use uuid in the future *) Albatross.query albatross ~domain:user.name ~name (`Unikernel_cmd `Unikernel_destroy) - >|= function + >>= function | Error msg -> Logs.err (fun m -> m "Error querying albatross: %s" msg); - let status = - { - Utils.Status.code = 500; - title = "Error"; - data = "Error querying albatross: " ^ msg; - success = false; - } - in - reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) - `Internal_server_error + http_response reqd ~status_code:500 ~title:"Error" + ~data:("Error querying albatross: " ^ msg) + ~success:false `Internal_server_error | Ok (_hdr, res) -> ( match Albatross_json.res res with | Ok res -> - let status = - { - Utils.Status.code = 200; - title = "Success"; - data = Yojson.Safe.to_string res; - success = true; - } - in - reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) - `OK + http_response reqd ~status_code:200 ~title:"Success" + ~data:(Yojson.Safe.to_string res) + ~success:true `OK | Error (`String res) -> - let status = - { - Utils.Status.code = 500; - title = "Error"; - data = Yojson.Safe.to_string (`String res); - success = false; - } - in - reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) - `Internal_server_error) + http_response reqd ~status_code:500 ~title:"Error" + ~data:(Yojson.Safe.to_string (`String res)) + ~success:false `Internal_server_error) let unikernel_create albatross reqd (user : User_model.user) = let response_body = Httpaf.Reqd.request_body reqd in @@ -880,34 +664,16 @@ struct match ct with | Error (`Msg msg) -> Logs.warn (fun m -> m "couldn't content-type: %S" msg); - let status = - { - Utils.Status.code = 400; - title = "Error"; - data = "Couldn't content-type: " ^ msg; - success = false; - } - in - Lwt.return - (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) - `Bad_request) + http_response reqd ~status_code:400 ~title:"Error" + ~data:("Couldn't content-type: " ^ msg) + ~success:false `Bad_request | Ok ct -> ( match Multipart_form.of_string_to_list data ct with | Error (`Msg msg) -> Logs.warn (fun m -> m "couldn't multipart: %s" msg); - let status = - { - Utils.Status.code = 400; - title = "Error"; - data = "Couldn't multipart: " ^ msg; - success = false; - } - in - Lwt.return - (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) - `Bad_request) + http_response reqd ~status_code:400 ~title:"Error" + ~data:("Couldn't multipart: " ^ msg) + ~success:false `Bad_request | Ok (m, assoc) -> ( let m, _r = to_map ~assoc m in match @@ -925,95 +691,44 @@ struct (* TODO use uuid in the future *) Albatross.query albatross ~domain:user.name ~name (`Unikernel_cmd (`Unikernel_create config)) - >|= function - | Error msg -> + >>= function + | Error err -> Logs.warn (fun m -> - m "error querying albatross: %s" msg); - let status = - { - Utils.Status.code = 500; - title = "Error"; - data = "Error while querying Albatross."; - success = false; - } - in - reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) - `Internal_server_error + m "Error querying albatross: %s" err); + + http_response reqd ~status_code:500 ~title:"Error" + ~data:("Error while querying Albatross: " ^ err) + ~success:false `Internal_server_error | Ok (_hdr, res) -> ( match Albatross_json.res res with | Ok res -> - let status = - { - Utils.Status.code = 200; - title = "Success"; - data = Yojson.Safe.to_string res; - success = true; - } - in - reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) - `OK + http_response reqd ~status_code:200 ~title:"Success" + ~data:(Yojson.Safe.to_string res) + ~success:true `OK | Error (`String res) -> - let status = - { - Utils.Status.code = 500; - title = "Error"; - data = Yojson.Safe.to_string (`String res); - success = false; - } - in - reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) - `Internal_server_error)) - | Error (`Msg msg) -> - Logs.warn (fun m -> m "couldn't decode data %s" msg); - let status = - { - Utils.Status.code = 500; - title = "Error"; - data = msg; - success = false; - } - in - Lwt.return - (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) - `Internal_server_error)) + http_response reqd ~status_code:500 ~title:"Error" + ~data:(Yojson.Safe.to_string (`String res)) + ~success:false `Internal_server_error)) + | Error (`Msg err) -> + Logs.warn (fun m -> m "couldn't decode data %s" err); + + http_response reqd ~status_code:500 ~title:"Error" ~data:err + ~success:false `Internal_server_error) | _ -> Logs.warn (fun m -> m "couldn't find fields"); - let status = - { - Utils.Status.code = 400; - title = "Error"; - data = "Couldn't find fields"; - success = false; - } - in - Lwt.return - (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) - `Bad_request))) + http_response reqd ~status_code:400 ~title:"Error" + ~data:"Couldn't find fields" ~success:false `Bad_request)) let unikernel_console albatross name reqd (user : User_model.user) = (* TODO use uuid in the future *) Albatross.query ~domain:user.name albatross ~name (`Console_cmd (`Console_subscribe (`Count 10))) >>= function - | Error msg -> - Logs.warn (fun m -> m "error querying albatross: %s" msg); - let status = - { - Utils.Status.code = 500; - title = "Error"; - data = "Error while querying Albatross: " ^ msg; - success = false; - } - in - Lwt.return - (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) - `Internal_server_error) + | Error err -> + Logs.warn (fun m -> m "error querying albatross: %s" err); + http_response reqd ~status_code:500 ~title:"Error" + ~data:("Error while querying Albatross: " ^ err) + ~success:false `Internal_server_error | Ok _ -> ( match Result.bind (Vmm_core.Name.path_of_string user.name) (fun domain -> @@ -1028,19 +743,10 @@ struct (reply reqd ~content_type:"application/json" (Yojson.Basic.to_string (`List data)) `OK) - | Error (`Msg msg) -> - let status = - { - Utils.Status.code = 500; - title = "Error"; - data = "Couldn't find create name " ^ String.escaped msg; - success = false; - } - in - Lwt.return - (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) - `Internal_server_error)) + | Error (`Msg err) -> + http_response reqd ~status_code:500 ~title:"Error" + ~data:("Error while querying Albatross: " ^ err) + ~success:false `Internal_server_error) let view_user albatross store uuid reqd (user : User_model.user) = let users = User_model.create_user_uuid_map (snd store).Storage.users in @@ -1090,19 +796,10 @@ struct (_ipaddr, _port) reqd = Lwt.async (fun () -> let bad_request () = - let status = - { - Utils.Status.code = 400; - title = "Error"; - data = "Bad HTTP request method."; - success = false; - } - in - Lwt.return - (reply reqd ~content_type:"application/json" - (Utils.Status.to_json status) - `Bad_request) + http_response reqd ~status_code:400 ~title:"Error" + ~data:"Bad HTTP request method." ~success:false `Bad_request in + let req = Httpaf.Reqd.request reqd in let path = Uri.(pct_decode (path (of_string req.Httpaf.Request.target))) From f0d3558be6a5e5f765dbc91bef6d5fcb5d33c859 Mon Sep 17 00:00:00 2001 From: Hannes Mehnert Date: Wed, 11 Sep 2024 12:08:44 +0200 Subject: [PATCH 18/21] Apply suggestions from code review --- unikernel.ml | 1 - 1 file changed, 1 deletion(-) diff --git a/unikernel.ml b/unikernel.ml index ab94789c..56648fab 100644 --- a/unikernel.ml +++ b/unikernel.ml @@ -300,7 +300,6 @@ struct Store.update_user !store user >>= function | Ok store' -> ( store := store'; - let cookie = List.find_opt (fun (c : User_model.cookie) -> From 45bd4e7e21fb3b194db0b82fac539a015437af7a Mon Sep 17 00:00:00 2001 From: Hannes Mehnert Date: Wed, 11 Sep 2024 12:09:41 +0200 Subject: [PATCH 19/21] Update unikernel.ml --- unikernel.ml | 1 - 1 file changed, 1 deletion(-) diff --git a/unikernel.ml b/unikernel.ml index 56648fab..72530f73 100644 --- a/unikernel.ml +++ b/unikernel.ml @@ -318,7 +318,6 @@ struct ("location", "/dashboard"); ] in - http_response reqd ~header_list ~status_code:200 ~title:"Success" ~data: From 7b731467b7478d614ae2c69d503dae2f93e4f867 Mon Sep 17 00:00:00 2001 From: Hannes Mehnert Date: Wed, 11 Sep 2024 12:13:00 +0200 Subject: [PATCH 20/21] Update unikernel.ml --- unikernel.ml | 1 - 1 file changed, 1 deletion(-) diff --git a/unikernel.ml b/unikernel.ml index 72530f73..a0453f0f 100644 --- a/unikernel.ml +++ b/unikernel.ml @@ -693,7 +693,6 @@ struct | Error err -> Logs.warn (fun m -> m "Error querying albatross: %s" err); - http_response reqd ~status_code:500 ~title:"Error" ~data:("Error while querying Albatross: " ^ err) ~success:false `Internal_server_error From eb4dac959a9daa3fb4910266186c75b2b24a87cf Mon Sep 17 00:00:00 2001 From: Hannes Mehnert Date: Wed, 11 Sep 2024 12:14:40 +0200 Subject: [PATCH 21/21] Update unikernel.ml --- unikernel.ml | 1 - 1 file changed, 1 deletion(-) diff --git a/unikernel.ml b/unikernel.ml index a0453f0f..5a548edb 100644 --- a/unikernel.ml +++ b/unikernel.ml @@ -796,7 +796,6 @@ struct http_response reqd ~status_code:400 ~title:"Error" ~data:"Bad HTTP request method." ~success:false `Bad_request in - let req = Httpaf.Reqd.request reqd in let path = Uri.(pct_decode (path (of_string req.Httpaf.Request.target)))