biff

jf 2024-07-20T10:20:15.711869Z

I am trying to understand things around the context map and the session and have a few questions: 1. what exactly is the context map anyway? 2. I see in the tutorial code that the context map is often used to store data: assoc ctx :community community , etc. I’m not sure if this is for convenience though, or if what’s stored in the context map this way is persistent. If I want data to persist for a session, should I store it in the session? 3. how is the context map stored (or not!), vs the session? 4. I see a :uid key in the session (that even matches with the uid for (:user ctx))… but don’t see any code in the tutorial that actually stores or assigns :uid for the session. How is :uid assigned?

jf 2024-07-21T13:52:29.498369Z

thank you, Jacob! I am getting a clearer picture now. Thank you for the links to the code! Without going into the details, I was considering adding some sort of data view tracker. I've had some time to rethink this though, and am still in the midst of thinking through implementation details

👍 1
jf 2024-07-20T13:20:16.599789Z

I'm guessing that the context map is basically generated anew for every request, and from https://biffweb.com/docs/reference/security/, it mentions that Sessions are stored in encrypted cookies... I guess that answers qn 3 (unless I'm wrong somehow about the context map). I'm currently looking at the source code of the authentication module now.. and it looks like the session is set using the :session key of the request map. This is simple enough for responses that return a response map. But what of the functions that return html?

2024-07-20T18:34:50.351129Z

The context map is a combination of things. It always includes the "system map", (i.e. the value of com.example/initial-system after it gets passed through all your biff components). The use-jetty component also merges incoming ring requests with the system map, so if you're inside of an http request handler, ctx will include stuff from the ring request like :params, :session, :headers etc. Middleware often inserts additional stuff into the context map, like how the tutorial does assoc ctx :community community in some of the middleware--so any request handler inside of that middleware can get :community from the ctx map instead of having each request handler query for that on its own. You can think of the context map as a big in-memory key-value store that's used by biff to help all the different parts of your system communicate with each other in a consistent way. e.g. you can add values to resources/config.edn and then access those values from your request handlers or anywhere else in your app. (I called it the "context" map because I wanted a general name for something that might include a bunch of different stuff. e.g. before using that name, I never knew whether to have my functions call it sys, or req, or opts, or ...) The context map is not persisted though; it just gets passed around in memory. i.e. it is more-or-less just for convenience as you say. Biff uses Ring's encrypted session cookie middleware (see https://github.com/ring-clojure/ring/wiki/Sessions#session-stores and https://ring-clojure.github.io/ring/ring.middleware.session.cookie.html). It's convenient to do it this way because you get persistence (with ring's default in-memory sessions, users would get logged out whenever you restart your app) without having to deal with any infrastructure (since you're letting the user's browser handle the persistence for you). A trade-off here is that the entire cookie contents get sent along with each request, and I'm pretty sure there's a limit on cookie size too. So you may not want to stick a ton of stuff in the session, but if you've just got a couple things that you want to expire when the session expires, it should be fine. :uid gets set (and unset) by the authentication plugin (see https://github.com/jacobobryant/biff/blob/32640d260c63a0260352fa5053feb30eb3c7c542/src/com/biffweb/impl/auth.clj#L172, https://github.com/jacobobryant/biff/blob/32640d260c63a0260352fa5053feb30eb3c7c542/src/com/biffweb/impl/auth.clj#L228 and https://github.com/jacobobryant/biff/blob/32640d260c63a0260352fa5053feb30eb3c7c542/src/com/biffweb/impl/auth.clj#L236). If you want to add more things to the session, the easiest way might be to wrap the authentication plugin routes with some custom middleware:

(defn wrap-add-stuff-to-session [handler]
  (fn [ctx]
    (let [response (handler ctx)]
      (if (-> response :session :uid)
        (update response :session assoc ...)
        ;; The authentication plugin doesn't remove the entire session when you
        ;; sign out; it just removes the :uid key from the session. I don't
        ;; remember if there's a reason it doesn't just delete the whole
        ;; session. Maybe I should change it to do that. In the mean time, you
        ;; can remove the session/individual keys from the session if the :uid
        ;; key isn't there:
        (update response :session dissoc ...)))))

(def modules
  [app/module
   (update (biff/authentication-module {})
           :routes
           (fn [routes]
             ["" {:middleware [wrap-add-stuff-to-session]}
              routes]))
Alternatively you could copy the authentication code into your project (e.g. as described in the https://biffweb.com/p/how-to-use-postgres-with-biff/) and then change the code directly. If you start wanting to do extra logic around sign-in/sign-out, you'll probably need to do that at some point anyway. I'd also be open to adding an option to the authentication plugin so you can pass in a callback that adds stuff to the session when the user signs in. > I'm guessing that the context map is basically generated anew for every request Partially, yes. The part of it that comes from the system map is the same for every request. The ring request part of it is created by jetty / ring / middleware on each request. Biff also sets the :biff/db key to an up-to-date snapshot of the database on each request. > But what of the functions that return html? If you have a handler like this:
(defn my-handler [ctx]
  [:div "hello"])
You can change to it to this:
(defn my-handler [{:keys [session] :as ctx}]
  {:status 200
   :headers {"content-type" "text/html"}
   :body (str "\n" (rum.core/render-static-markup [:div "hello"]))
   :session (assoc session ...)})
The first form is just a convenience for the common case when you just want a regular 200 html response. It gets expanded to the second form by the https://github.com/jacobobryant/biff/blob/32640d260c63a0260352fa5053feb30eb3c7c542/src/com/biffweb/impl/middleware.clj#L34 middleware.

1
2024-07-20T18:36:47.781729Z

What are you trying to add to the session by the way? just curious.

macrobartfast 2024-07-20T21:00:43.441699Z

I’m trying to interact with S3 from the repl during dev along the lines of

(:body (s3-request ctx {:method "GET"
                        :key "some-key"}))
per https://biffweb.com/docs/api/utilities/ How do I get ctx again? And do I create secrets.env manually? I only ask because there is a clj <create secrets.env> command I think.

macrobartfast 2024-07-24T22:48:35.433199Z

Sweet! Thanks!!

👌 1
2024-07-22T23:52:48.033789Z

I just noticed the s3-request docstring is out-of-date. For recentlyish-created projects you'll have a config.env and a resources/config.edn instead of secrets.env and config.edn. And then you'd do this:

# config.env
S3_SECRET_KEY="your-secret-key"

;; resources/config.edn
{:biff.s3/origin ""
 :biff.s3/access-key "your-access-key"
 :biff.s3/secret-key #biff/env "S3_SECRET_KEY"
 :biff.s3/bucket "default-bucket"
 ...

;; Put an object:
(s3-request ctx {:method "PUT"
                 :key "some-key"
                 :body "some-body"
                 :headers {"x-amz-acl" "private"
                           "content-type" "text/plain"}})

;; Get an object:
(:body (s3-request ctx {:method "GET"
                        :key "some-key"}))

2024-07-22T23:55:40.068309Z

usually, you'll receive ctx as a parameter. e.g. if you're inside a ring handler, ctx will be merged with the ring request, so you can do e.g.

(defn handler [ctx]
  (let [object (:body (biff/s3-request ctx {:method "GET" :key "some-key"}))]
    ...))
But if you're in the repl, you'll instead need to get it with (biff/merge-context @com.example/system). The repl.clj file has a get-context helper function which does that for you.