Skip to the content.

Authentication

Joy doesn’t have an authentication library (yet), but in the mean time here is a pretty good example of how you can add sign up/sign in/sign out to your project

Creating a database table for accounts

The first step in this unfortunate multi-step process is to create a new table “account” with email and password columns:

joy create table account 'email text unique not null' 'password text not null'
joy migrate

Don’t forget to migrate!

Create a password key for hashing passwords

Open up your janet repl and type this in

(import cipher)
(cipher/password-key)

Copy and paste the output to your .env file like this:

# .env
PASSWORD_KEY=<copy-pasted-value>

Replace <copy-pasted-value> with the actual value.

Look, I know you know, but I just want to make sure this isn’t a known unknown or an unknown unknown situation. So I spelled it out.

Creating new accounts

The next step is to set up routes and handlers to let people sign up and one more route to redirect people after signing in/up.

For this next bit you’re going to need another library in your project.janet file, cipher for hashing the passwords.

src/routes/account.janet

(use joy)
(import cipher)


(def account-params
     (params :account
             (validates [:email :password] :required true)
             (permit [:email :password])))


(defn new [request]
  (let [account (get request :account {})]
    (form-for [request :account/create]
      (label :email "email")
      (email-field account :email)

      (label :password "password")
      (password-field account :password)

      (label :confirm-password "confirm your password")
      (password-field account :confirm-password)

      (submit "save"))))


(defn hash-password [dict]
  (let [{:password password} dict
        key (env :password-key)
        new-password (cipher/hash-password key password)
    (merge dict {:password new-password})))


(defn create [request]
  (let [result (as-> (account-params request) ?
                     (hash-password ?)
                     (db/insert ?)
                     (rescue-from :params ?))
        [errors account] result
        account (select-keys account [:email])]

    (if errors
      (new (put request :errors errors))
      (-> (redirect-to :home/index)
          (put :session {:account account})))))

Then over in your routes file add those two handlers

src/routes.janet

(use joy)
(import ./src/routes/home :as home)
(import ./src/routes/account :as account)

(defroutes routes
  [:get "/" home/index]
  [:get "/sign-up" account/create]
  [:post "/accounts" account/create])

Hashing passwords

In case you didn’t catch it before, this is the bit where we hash passwords. It’s kind of involved, but it works.

(defn hash-password [dict]
  (let [{:password password} dict
        key (env :password-key)
        new-password (cipher/hash-password key password)
    (merge dict {:password new-password})))

Signing accounts in

src/routes/session.janet

(use joy)
(import cipher)


(def account-params
  (params :account
    (validates [:email :password] :required true)
    (permit [:email :password])))


(defn new [request]
  (let [account (get request :account {})
        errors (get request :errors {})]

    (form-for [request :session/create]
      (label :email)
      (email-field account :email)
      (when errors [:div {:class "red"} (get errors :email)])

      (label :password)
      (password-field account :password)
      (when errors [:div {:class "red"} (get errors :password)])

      (submit "Save")))))


(defn password-matches? [hashed plaintext]
  (cipher/verify-password (env :password-key)
                          (get hashed :password)
                          (get plaintext :password)))


(defn create [request]
  (let [[_ account-params] (rescue (account-params request))
        email (get account-params :email)
        account (-> (db/from :account :where {:email email} :limit 1)
                    (get 0))]

    (if (and (not (nil? account))
             (password-matches? account account-params))
      (-> (redirect-to :home/index)
          (put :session {:account (select-keys account [:email])}))
      (new (put request :errors {:email "Email or password is incorrect"})))))

Don’t forget to wire up those routes:

(use joy)
(import ./src/routes/home :as home)
(import ./src/routes/account :as account)
(import ./src/routes/session :as session)

(defroutes routes
  [:get "/" home/index]
  [:get "/sign-up" account/create]
  [:post "/accounts" account/create]
  [:get "/sign-in" session/create]
  [:post "/sessions" session/create])

Signing accounts out

(defn destroy [request]
  (-> (redirect-to :home/index)
      (put :session {})))

One day this will all be a bad dream and you’ll be able to stick one line of authentication middleware in there and have working email/password auth (and possibly google/apple auth as well).

Oh, don’t forget to update the routes again

(use joy)
(import ./src/routes/home :as home)
(import ./src/routes/account :as account)
(import ./src/routes/session :as session)

(defroutes routes
  [:get "/" home/index]
  [:get "/sign-up" account/create]
  [:post "/accounts" account/create]
  [:get "/sign-in" session/create]
  [:post "/sessions" session/create]
  [:delete "/sessions" session/destroy]) ; add this one here