RSS Feed

Lisp Project of the Day

lack-middleware-session

You can support this project by donating at:

Donate using PatreonDonate using Liberapay

Or see the list of project sponsors.

lack-middleware-sessionweb

Documentation🥺
Docstrings🥺
Tests 😀
Examples🥺
RepositoryActivity😀
CI 😀

This middleware makes your app stateful and allows to associate some information with the current user.

There are two abstractions behind Lack sessions - state and store.

State object defines how to keep track of a session. Lack includes only one type of state class. It keeps state id in the browser's cookies.

Store object defines where to store data, associated with a state. There are three store classes in the Lack. The default stores data in memory, using a hash table. There are also dbi and redis stores.

Now let's create an app that allows a user to login in and logout.

First, we need an app for logging it checks the password as we did in yesterday's post on basic auth. If the password is correct, we'll put a user's login into a session's hash:

POFTHEDAY> (defun login (env)
             (let* ((params (getf env :body-parameters))
                    (login (alexandria:assoc-value
                            params
                            "login" :test #'string=))
                    (password (alexandria:assoc-value
                               params
                               "password" :test #'string=))
                    (session (getf env
                                   :lack.session)))
               (cond
                 ((and (string= login
                                "bob")
                       (string= password
                                "$ecret"))
                  (setf (gethash :login
                                 session)
                        login)
                  '(200 (:content-type "text/plain")
                    ("Dear Bob, you welcome!")))
                 (t
                  '(200 (:content-type "text/plain")
                    ("Wrong password!"))))))

Also, we need a function to logout. It set's a special flag to let middleware know that all session data should be wiped from the store:

POFTHEDAY> (defun logout (env)
             (setf (getf (getf env :lack.session.options)
                         :expire)
                   t)
             '(200 (:content-type "text/plain")
               ("Now you are logged our")))

The main app will use data from the session and will show a welcome message if the user is authenticated:

POFTHEDAY> (defun main (env)
             (let* ((session (getf env :lack.session))
                    (login (gethash :login session)))
               (cond
                 (login
                  (list 200 (list :content-type "text/plain")
                        (list (format nil "Welcome, ~A!"
                                      login))))
                 (t
                  '(403 (:content-type "text/plain")
                        ("Access denied"))))))

And finally, we need to combine these apps using mount middleware (it was reviewed a few days ago) and slap the session middleware on it:

POFTHEDAY> (clack:clackup
            (lack:builder
             :session
             (:mount "/login" 'login)
             (:mount "/logout" 'logout)
             'main)
            :port 8089)
Hunchentoot server is started.
Listening on 127.0.0.1:8089.

Now let's try to log in:

POFTHEDAY> (values (dex:get "http://localhost:8090/"))
"Access denied"

POFTHEDAY> (multiple-value-bind (response code headers)
               (dex:post "http://localhost:8090/login"
                         :content '(("login" . "bob")
                                    ("password" . "$ecret")))
             (values response code
                     (rutils:hash-table-to-alist headers)))
"Dear Bob, you welcome!"
200
(("date" . "Sat, 27 Jun 2020 20:47:13 GMT")
 ("server" . "Hunchentoot 1.2.38")
 ("transfer-encoding" . "chunked")
 ("content-type" . "text/plain")
 ("set-cookie"
  "lack.session=b10c66; path=/; expires=Fri, 23 Dec 2140 17:24:51 GMT"))

The server returned the "set-cookie" header. Usually, the browser will pass this cookie content during the following requests. We'll emulate this behavior to make a request to the main app:

POFTHEDAY> (let ((headers '((:cookie . "lack.session=b10c66"))))
             (values (dex:get "http://localhost:8090/"
                              :headers headers)))
"Welcome, bob!"

And finally, we'll check how does log out will work:

POFTHEDAY> (let ((headers '((:cookie . "lack.session=b10c66"))))
             (values (dex:post "http://localhost:8090/logout"
                               :headers headers)))
"Now you are logged out"

POFTHEDAY> (let ((headers '((:cookie . "lack.session=b10c66"))))
             (dex:get "http://localhost:8090/"
                              :headers headers))
"Access denied"

See!? We've built a simple web application using Lack micro-framework! Add something like Spinneret to render HTML and Lass + Parenscript to render CSS and JS and we'll have a full-fledged webapp!


Brought to you by 40Ants under Creative Commons License