Skip to content

Commit

Permalink
Implementing Middlewares (#10)
Browse files Browse the repository at this point in the history
* check if a user already exists

* deode_request_body to dedicated function

* add sign in url

* add sign in page

* add sign in logic

* refactor

* handle login of users

* fix typo

* javascript to call login endpoints

* add cookies for users

* fix styling issues on frontend sign up

* stop tracking keys

* update users on disk

* add auth control cookies and middleware checks

* add auth middleware functionality

* use emile to validate emails

* refactor according to reviews

* lint

* add a utils module to handle commonly used code

* update styles
  • Loading branch information
PizieDust authored Jul 16, 2024
1 parent d7f0653 commit 28eebd8
Show file tree
Hide file tree
Showing 10 changed files with 752 additions and 70 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ dune-workspace
dune.build
dune.config
Makefile
mirage
mirage
keys
2 changes: 1 addition & 1 deletion assets/style.css

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions config.ml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ let mollymawk =
package "multipart_form";
package "mirage-crypto-rng";
package "uuidm";
package "emile";
package "paf" ~sublibs:[ "mirage" ] ~min:"0.5.0";
package "oneffs";
]
Expand Down
84 changes: 84 additions & 0 deletions middleware.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
type handler = Httpaf.Reqd.t -> unit Lwt.t
type middleware = handler -> handler

let has_session_cookie (reqd : Httpaf.Reqd.t) : bool =
let headers = (Httpaf.Reqd.request reqd).headers in
match Httpaf.Headers.get headers "Cookie" with
| Some cookies ->
let cookie_list = String.split_on_char ';' cookies in
List.exists
(fun cookie ->
let parts = String.trim cookie |> String.split_on_char '=' in
match parts with
| [ name; _ ] -> String.equal name "molly_session"
| _ -> false)
cookie_list
| _ -> false

let apply_middleware middlewares handler =
List.fold_right (fun middleware acc -> middleware acc) middlewares handler

let redirect_to_login reqd ?(msg = "") () =
let headers = Httpaf.Headers.of_list [ ("location", "/sign-in") ] in
let response = Httpaf.Response.create ~headers `Found in
Httpaf.Reqd.respond_with_string reqd response msg;
Lwt.return_unit

let redirect_to_dashboard reqd ?(msg = "") () =
let headers = Httpaf.Headers.of_list [ ("location", "/dashboard") ] in
let response = Httpaf.Response.create ~headers `Found in
Httpaf.Reqd.respond_with_string reqd response msg;
Lwt.return_unit

let auth_middleware ~users handler reqd =
let headers = (Httpaf.Reqd.request reqd).headers in
match Httpaf.Headers.get headers "Cookie" with
| Some cookies -> (
let cookie_list = String.split_on_char ';' cookies in
let session_cookie =
List.find_opt
(fun cookie ->
let parts = String.trim cookie |> String.split_on_char '=' in
match parts with
| [ name; _ ] -> String.equal name "molly_session"
| _ -> false)
cookie_list
in
match session_cookie with
| Some cookie -> (
let parts = String.trim cookie |> String.split_on_char '=' in
let value = List.nth parts 1 in
match User_model.find_user_by_key value users with
| Some user -> (
let user_session =
List.find_opt
(fun (cookie : User_model.cookie) ->
String.equal cookie.name "molly_session")
user.cookies
in
match user_session with
| Some cookie -> (
match String.equal cookie.value value with
| true -> handler reqd
| false ->
Logs.err (fun m ->
m
"auth-middleware: Session value doesn't match user \
session %s\n"
value);
redirect_to_login reqd ())
| None ->
Logs.err (fun m ->
m "auth-middleware: User doesn't have a session cookie.\n");
redirect_to_login reqd ())
| None ->
Logs.err (fun m ->
m "auth-middleware: Failed to find user with key %s\n" value);
redirect_to_login reqd ())
| None ->
Logs.err (fun m ->
m "auth-middleware: No molly-session in cookie header.");
redirect_to_login reqd ())
| _ ->
Logs.err (fun m -> m "auth-middleware: No Cookie in request headers.\n");
redirect_to_login reqd ()
277 changes: 277 additions & 0 deletions sign_in.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
open Tyxml

let login_page ~icon () =
let page =
Html.(
html
(Header_layout.header ~page_title:"Sign in | Mollymawk" ~icon ())
(body
[
section
~a:[ a_class [ "max-w-7xl relative px-5 mx-auto" ] ]
[
div
~a:[ a_class [ "absolute" ] ]
[
img
~a:[ a_class [ "md:w-20 w-14" ] ]
~src:"/images/robur.png" ~alt:"Robur.coop" ();
];
];
main
~a:
[
a_class
[
"w-full grid md:grid-cols-3 grid-cols-1 text-gray-700 \
md:h-screen";
];
]
[
section
~a:
[
a_class
[
"h-full flex justify-center items-center col-span-2 \
md:py-10 py-5";
];
]
[
div
~a:[ a_class [ "w-full max-w-lg mt-16 pb-16 mx-auto" ] ]
[
h1
~a:
[
a_class
[
"font-semibold text-2xl md:text-3xl mb-8 \
text-primary-800 p-6";
];
]
[ txt "Sign in to Your Account" ];
div
~a:[ a_class [ "space-y-6 mt-8 shadow-md p-6" ] ]
[
p ~a:[ a_id "form-alert" ] [];
div
[
label
~a:
[
a_class [ "block text-sm font-medium" ];
a_label_for "email";
]
[ txt "Email Address*" ];
input
~a:
[
a_autocomplete `On;
a_input_type `Text;
a_name "email";
a_id "email";
a_class
[
"ring-primary-100 mt-1.5 transition \
appearance-none block w-full px-3 \
py-3 rounded-xl shadow-sm border \
hover:border-primary-200\n\
\ \
focus:border-primary-300 \
bg-primary-50 bg-opacity-0 \
hover:bg-opacity-50 \
focus:bg-opacity-50 \
ring-primary-200 \
focus:ring-primary-200\n\
\ \
focus:ring-[1px] \
focus:outline-none";
];
]
();
p ~a:[ a_id "email-alert" ] [];
];
div
[
label
~a:
[
a_class [ "block text-sm font-medium" ];
a_label_for "password";
]
[ txt "Password*" ];
input
~a:
[
a_input_type `Password;
a_name "password";
a_id "password";
a_class
[
"ring-primary-100 mt-1.5 transition \
appearance-none block w-full px-3 \
py-3 rounded-xl shadow-sm border \
hover:border-primary-200\n\
\ \
focus:border-primary-300 \
bg-primary-50 bg-opacity-0 \
hover:bg-opacity-50 \
focus:bg-opacity-50 \
ring-primary-200 \
focus:ring-primary-200\n\
\ \
focus:ring-[1px] \
focus:outline-none";
];
]
();
p ~a:[ a_id "password-alert" ] [];
];
div
~a:
[
a_class
[
"flex items-center justify-between \
text-sm font-medium text-primary-500";
];
]
[
a
~a:
[
a_class
[
"hover:text-primary-800 \
transition-colors cursor-pointer";
];
]
[ txt "Forgot Your Password?" ];
a
~a:
[
a_href "/sign-up";
a_class
[
"hover:text-primary-800 \
transition-colors cursor-pointer";
];
]
[ txt "Need an account?" ];
];
div
[
button
~a:
[
a_id "login-button";
a_class
[
"py-3 rounded bg-primary-500 \
hover:bg-primary-800 w-full \
text-gray-50 font-semibold";
];
]
[ txt "Sign In" ];
];
];
];
];
aside
~a:
[
a_class
[ "relative h-full p-16 col-span-1 md:block hidden" ];
]
[
img ~src:"/images/molly_bird.jpeg"
~alt:"Mollymawk delivering unikernels"
~a:
[
a_class
[
"absolute inset-1 max-w-none w-full h-full \
object-cover";
];
]
();
];
];
Footer_layout.footer;
Tyxml.Html.Unsafe.data
"<script>\n\
\ const loginButton = \
document.getElementById('login-button')\n\
\ loginButton.addEventListener('click', async \
function() {\n\
\ const email = \
document.getElementById('email').value.toLowerCase()\n\
\ const password = \
document.getElementById('password').value\n\
\ let form_alert = \
document.getElementById('form-alert')\n\
\ let email_alert = \
document.getElementById('email-alert')\n\
\ let password_alert = \
document.getElementById('password-alert')\n\
\ form_alert.classList.add('hidden')\n\
\ email_alert.classList.add('hidden')\n\
\ password_alert.classList.add('hidden')\n\
\ if (!email || !password) {\n\
\ form_alert.classList.remove('hidden')\n\
\ \
form_alert.classList.add('text-secondary-500', 'block')\n\
\ form_alert.textContent = 'All fields are \
required'\n\
\ return;\n\
\ }\n\
\ const emailPattern = \
/^[a-zA-Z0-9.$_!]+@[a-zA-Z0-9]+\\.[a-z]{2,3}$/;\n\
\ if (!emailPattern.test(email)) {\n\
\ email_alert.classList.remove('hidden')\n\
\ \
email_alert.classList.add('text-secondary-500', 'block')\n\
\ email_alert.textContent = 'Please enter \
a valid email address.'\n\
\ return;\n\
\ }\n\
\ if (password.length < 8) {\n\
\ password_alert.classList.remove('hidden')\n\
\ \
password_alert.classList.add('text-secondary-500', 'block')\n\
\ password_alert.textContent = 'Password \
must be at least 8 characters long.'\n\
\ return;\n\
\ }\n\
\ try {\n\
\ const response = await \
fetch('/api/login', {\n\
\ method: 'POST',\n\
\ headers: {\n\
\ 'Content-Type': \
'application/json',\n\
\ },\n\
\ body: \
JSON.stringify({email, password })\n\
\ })\n\
\ const data = await response.json();\n\
\ if (data.status === 200) {\n\
\ window.location.replace('/dashboard')\n\
\ } else {\n\
\ form_alert.classList.remove('hidden')\n\
\ \
form_alert.classList.add('text-secondary-500', 'block')\n\
\ form_alert.textContent = data.message\n\
\ }\n\
\ } catch (error) {\n\
\ form_alert.classList.remove('hidden')\n\
\ \
form_alert.classList.add('text-secondary-500', 'block')\n\
\ form_alert.textContent = error\n\
\ return;\n\
\ }})\n\
\ </script>";
]))
in
Format.asprintf "%a" (Html.pp ()) page
Loading

0 comments on commit 28eebd8

Please sign in to comment.